├── src ├── animation │ ├── modules │ │ ├── index.ts │ │ └── Mount.tsx │ ├── hooks │ │ ├── index.ts │ │ ├── useMount.ts │ │ └── useValue.ts │ ├── index.ts │ ├── to.ts │ ├── helpers.ts │ ├── Config.ts │ ├── types.ts │ ├── descriptors.ts │ └── drivers.ts ├── hooks │ ├── index.ts │ ├── events │ │ └── useOutsideClick.ts │ └── observers │ │ ├── useInView.ts │ │ └── useScrollProgress.ts ├── gestures │ ├── hooks │ │ ├── index.ts │ │ ├── useDrag.ts │ │ ├── useMove.ts │ │ ├── useWheel.ts │ │ ├── useScroll.ts │ │ └── useRecognizer.ts │ └── controllers │ │ ├── Gesture.ts │ │ ├── WheelGesture.ts │ │ ├── ScrollGesture.ts │ │ ├── MoveGesture.ts │ │ └── DragGesture.ts ├── index.ts └── utils │ └── index.ts ├── .vscode └── settings.json ├── example ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── index.tsx │ ├── stories │ │ ├── examples │ │ │ ├── Svg.stories.ts │ │ │ ├── Loop.stories.ts │ │ │ ├── InView.stories.ts │ │ │ ├── Modal.stories.ts │ │ │ ├── Slider.stories.ts │ │ │ ├── Toast.stories.ts │ │ │ ├── Sorting.stories.ts │ │ │ ├── Stagger.stories.ts │ │ │ ├── TodoList.stories.ts │ │ │ ├── Ripple.stories.ts │ │ │ ├── SnapPoints.stories.ts │ │ │ ├── SharedElement.stories.ts │ │ │ ├── Svg.tsx │ │ │ ├── Loop.tsx │ │ │ ├── InView.tsx │ │ │ ├── Stagger.tsx │ │ │ ├── SnapPoints.tsx │ │ │ ├── Modal.tsx │ │ │ ├── Sorting.tsx │ │ │ ├── Ripple.tsx │ │ │ ├── Toast.tsx │ │ │ ├── Slider.tsx │ │ │ ├── TodoList.tsx │ │ │ └── SharedElement.tsx │ │ ├── animations │ │ │ ├── hooks │ │ │ │ ├── useValue │ │ │ │ │ ├── Array.stories.tsx │ │ │ │ │ ├── Object.stories.tsx │ │ │ │ │ ├── String.stories.tsx │ │ │ │ │ ├── BasicSetup.stories.tsx │ │ │ │ │ ├── AnimationControls.stories.tsx │ │ │ │ │ ├── String.tsx │ │ │ │ │ ├── BasicSetup.tsx │ │ │ │ │ ├── AnimationControls.tsx │ │ │ │ │ ├── Array.tsx │ │ │ │ │ └── Object.tsx │ │ │ │ └── useMount │ │ │ │ │ ├── BasicSetup.stories.ts │ │ │ │ │ ├── Modifiers.stories.ts │ │ │ │ │ ├── MultiValued.stories.ts │ │ │ │ │ ├── BasicSetup.tsx │ │ │ │ │ ├── Modifiers.tsx │ │ │ │ │ └── MultiValued.tsx │ │ │ └── modules │ │ │ │ └── Mount │ │ │ │ ├── Modifiers.stories.ts │ │ │ │ ├── BasicSetup.stories.ts │ │ │ │ ├── MultiValued.stories.ts │ │ │ │ ├── BasicSetup.tsx │ │ │ │ ├── Modifiers.tsx │ │ │ │ └── MultiValued.tsx │ │ └── gestures │ │ │ └── hooks │ │ │ ├── useDrag │ │ │ ├── BasicSetup.stories.ts │ │ │ ├── MultipleElements.stories.ts │ │ │ ├── ConditionalBinding.stories.ts │ │ │ ├── BasicSetup.tsx │ │ │ ├── ConditionalBinding.tsx │ │ │ └── MultipleElements.tsx │ │ │ ├── useMove │ │ │ ├── BasicSetup.stories.ts │ │ │ ├── SingleElement.stories.ts │ │ │ ├── MultipleElements.stories.ts │ │ │ ├── BasicSetup.tsx │ │ │ ├── SingleElement.tsx │ │ │ └── MultipleElements.tsx │ │ │ ├── useWheel │ │ │ ├── BasicSetup.stories.ts │ │ │ └── BasicSetup.tsx │ │ │ └── useScroll │ │ │ ├── BasicSetup.stories.ts │ │ │ ├── ScrollTrigger.stories.ts │ │ │ ├── BasicSetup.tsx │ │ │ └── ScrollTrigger.tsx │ └── index.css ├── .gitignore ├── .storybook │ ├── preview.ts │ └── main.ts ├── tsconfig.json ├── package.json └── README.md ├── .prettierrc ├── .npmignore ├── .releaserc.json ├── rollup.config.mjs ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ ├── release.yml │ └── release-next.yml ├── .gitignore ├── package.json └── README.md /src/animation/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { Mount } from './Mount'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compile-hero.disable-compile-files-on-did-save-code": false 3 | } 4 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dipeshrai123/react-ui-animate/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dipeshrai123/react-ui-animate/HEAD/example/public/logo192.png -------------------------------------------------------------------------------- /example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dipeshrai123/react-ui-animate/HEAD/example/public/logo512.png -------------------------------------------------------------------------------- /src/animation/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useValue } from './useValue'; 2 | export { useMount } from './useMount'; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useOutsideClick } from './events/useOutsideClick'; 2 | export { useInView } from './observers/useInView'; 3 | -------------------------------------------------------------------------------- /src/animation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './modules'; 3 | export * from './descriptors'; 4 | export * from './Config'; 5 | export * from './to'; 6 | -------------------------------------------------------------------------------- /src/gestures/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useDrag } from './useDrag'; 2 | export { useMove } from './useMove'; 3 | export { useScroll } from './useScroll'; 4 | export { useWheel } from './useWheel'; 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | example/ 3 | .git/ 4 | .github/ 5 | .vscode/ 6 | *.log 7 | .gitignore 8 | .npmignore 9 | /src 10 | package-lock.json 11 | tsconfig.json 12 | *.mjs 13 | .prettierrc 14 | CHANGELOG.md 15 | .idea/ 16 | .releaserc.json -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | 3 | import './index.css'; 4 | 5 | const App = () =>
RUN WITH STORYBOOK
; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | 11 | root.render(); 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | combine, 3 | Easing, 4 | makeMotion as makeAnimated, 5 | motion as animate, 6 | } from '@raidipesh78/re-motion'; 7 | 8 | export * from './animation'; 9 | 10 | export * from './hooks'; 11 | 12 | export * from './gestures/hooks'; 13 | 14 | export * from './utils'; 15 | -------------------------------------------------------------------------------- /src/animation/to.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ExtrapolateConfig, 3 | to as interpolate, 4 | } from '@raidipesh78/re-motion'; 5 | 6 | export function to( 7 | input: number, 8 | inRange: number[], 9 | outRange: (number | string)[], 10 | config?: ExtrapolateConfig 11 | ): number | string { 12 | return interpolate(inRange, outRange, config)(input); 13 | } 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/Svg.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Svg'; 4 | 5 | const meta = { 6 | title: 'Examples/Svg', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Svg: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/Loop.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Loop'; 4 | 5 | const meta = { 6 | title: 'Examples/Loop', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Loop: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/InView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './InView'; 4 | 5 | const meta = { 6 | title: 'Examples/InView', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const InView: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/Modal.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Modal'; 4 | 5 | const meta = { 6 | title: 'Examples/Modal', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Modal: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/Slider.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Slider'; 4 | 5 | const meta = { 6 | title: 'Examples/Slider', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Slider: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/Toast.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Toast'; 4 | 5 | const meta = { 6 | title: 'Examples/Toast', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Toast: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/Sorting.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Sorting'; 4 | 5 | const meta = { 6 | title: 'Examples/Sorting', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Sorting: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/Stagger.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Stagger'; 4 | 5 | const meta = { 6 | title: 'Examples/Stagger', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Stagger: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/TodoList.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './TodoList'; 4 | 5 | const meta = { 6 | title: 'Examples/TodoList', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const TodoList: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/Ripple.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Ripple'; 4 | 5 | const meta = { 6 | title: 'Examples/RippleButton', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const RippleButton: Story = {}; 14 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main", { "name": "next", "prerelease": true }], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | [ 7 | "@semantic-release/npm", 8 | { 9 | "npmPublish": true, 10 | "npmTag": "next" 11 | } 12 | ], 13 | "@semantic-release/github" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /example/src/stories/examples/SnapPoints.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './SnapPoints'; 4 | 5 | const meta = { 6 | title: 'Examples/SnapPoints', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const SnapPoints: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/Array.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Array'; 4 | 5 | const meta = { 6 | title: 'Animations/Hooks/useValue', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Array: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/Object.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Object'; 4 | 5 | const meta = { 6 | title: 'Animations/Hooks/useValue', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Object: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/String.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './String'; 4 | 5 | const meta = { 6 | title: 'Animations/Hooks/useValue', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const String: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/examples/SharedElement.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './SharedElement'; 4 | 5 | const meta = { 6 | title: 'Examples/SharedElement', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const SharedElement: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/modules/Mount/Modifiers.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Modifiers'; 4 | 5 | const meta = { 6 | title: 'Animations/Modules/Mount', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Modifiers: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useDrag/BasicSetup.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './BasicSetup'; 4 | 5 | const meta = { 6 | title: 'Gestures/Hooks/useDrag', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const BasicSetup: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useMove/BasicSetup.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './BasicSetup'; 4 | 5 | const meta = { 6 | title: 'Gestures/Hooks/useMove', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const BasicSetup: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useWheel/BasicSetup.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './BasicSetup'; 4 | 5 | const meta = { 6 | title: 'Gestures/hooks/useWheel', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const BasicSetup: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useMount/BasicSetup.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './BasicSetup'; 4 | 5 | const meta = { 6 | title: 'Animations/Hooks/useMount', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const BasicSetup: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useMount/Modifiers.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './Modifiers'; 4 | 5 | const meta = { 6 | title: 'Animations/Hooks/useMount', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Modifiers: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/BasicSetup.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './BasicSetup'; 4 | 5 | const meta = { 6 | title: 'Animations/Hooks/useValue', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const BasicSetup: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/modules/Mount/BasicSetup.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './BasicSetup'; 4 | 5 | const meta = { 6 | title: 'Animations/Modules/Mount', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const BasicSetup: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/modules/Mount/MultiValued.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './MultiValued'; 4 | 5 | const meta = { 6 | title: 'Animations/Modules/Mount', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const MultiValued: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useScroll/BasicSetup.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './BasicSetup'; 4 | 5 | const meta = { 6 | title: 'Gestures/hooks/useScroll', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const BasicSetup: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useMount/MultiValued.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './MultiValued'; 4 | 5 | const meta = { 6 | title: 'Animations/Hooks/useMount', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const MultiValued: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useMove/SingleElement.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './SingleElement'; 4 | 5 | const meta = { 6 | title: 'Gestures/Hooks/useMove', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const SingleElement: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useScroll/ScrollTrigger.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './ScrollTrigger'; 4 | 5 | const meta = { 6 | title: 'Gestures/hooks/useScroll', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const ScrollTrigger: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useDrag/MultipleElements.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './MultipleElements'; 4 | 5 | const meta = { 6 | title: 'Gestures/Hooks/useDrag', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const MultipleElements: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useMove/MultipleElements.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './MultipleElements'; 4 | 5 | const meta = { 6 | title: 'Gestures/Hooks/useMove', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const MultipleElements: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useDrag/ConditionalBinding.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './ConditionalBinding'; 4 | 5 | const meta = { 6 | title: 'Gestures/Hooks/useDrag', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const ConditionalBinding: Story = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/AnimationControls.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Example from './AnimationControls'; 4 | 5 | const meta = { 6 | title: 'Animations/Hooks/useValue', 7 | component: Example, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const AnimationControls: Story = {}; 14 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | *storybook.log -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | import pkg from "./package.json" with { type: "json" }; 5 | 6 | export default { 7 | input: "src/index.ts", 8 | output: [ 9 | { 10 | file: pkg.main, 11 | format: "cjs", 12 | exports: "named", 13 | sourcemap: true, 14 | strict: false, 15 | }, 16 | ], 17 | plugins: [typescript(), terser()], 18 | external: ["react", "react-dom"], 19 | }; 20 | -------------------------------------------------------------------------------- /src/gestures/hooks/useDrag.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | 3 | import { 4 | type DragConfig, 5 | type DragEvent, 6 | DragGesture, 7 | } from '../controllers/DragGesture'; 8 | import { useRecognizer } from './useRecognizer'; 9 | 10 | export function useDrag( 11 | refs: RefObject | Array>, 12 | onDrag: (e: DragEvent & { index: number }) => void, 13 | config?: DragConfig 14 | ): void { 15 | return useRecognizer(DragGesture, refs, onDrag, config); 16 | } 17 | -------------------------------------------------------------------------------- /src/animation/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { Descriptor } from './types'; 2 | 3 | export function filterCallbackOptions( 4 | options: Record = {}, 5 | attach: boolean 6 | ) { 7 | if (attach) return options; 8 | const { onStart, onChange, onComplete, ...rest } = options; 9 | return rest; 10 | } 11 | 12 | export function isDescriptor(x: unknown): x is Descriptor { 13 | return ( 14 | typeof x === 'object' && 15 | x !== null && 16 | 'type' in x && 17 | typeof (x as any).type === 'string' 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/gestures/hooks/useMove.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | 3 | import { type MoveEvent, MoveGesture } from '../controllers/MoveGesture'; 4 | import { useRecognizer } from './useRecognizer'; 5 | 6 | export function useMove( 7 | refs: Window, 8 | onMove: (e: MoveEvent & { index: 0 }) => void 9 | ): void; 10 | 11 | export function useMove( 12 | refs: RefObject | Array>, 13 | onMove: (e: MoveEvent & { index: number }) => void 14 | ): void; 15 | 16 | export function useMove(refs: any, onMove: any): void { 17 | return useRecognizer(MoveGesture, refs, onMove); 18 | } 19 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | import '../src/index.css'; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | options: { 8 | storySort: { 9 | method: 'alphabetical', 10 | order: [ 11 | 'Animations', 12 | ['Hooks', ['useValue', 'useMount']], 13 | 'Gestures', 14 | '*', 15 | ], 16 | }, 17 | }, 18 | controls: { 19 | matchers: { 20 | color: /(background|color)$/i, 21 | date: /Date$/i, 22 | }, 23 | }, 24 | }, 25 | }; 26 | 27 | export default preview; 28 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useScroll/BasicSetup.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useScroll } from 'react-ui-animate'; 3 | 4 | const Example = () => { 5 | const [scrollPosition, setScrollPosition] = useState(0); 6 | 7 | useScroll(window, function (event) { 8 | console.log('Scroll', event); 9 | setScrollPosition(event.offset.y); 10 | }); 11 | 12 | return ( 13 |
14 |
15 | SCROLL POSITION: {scrollPosition} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Example; 22 | -------------------------------------------------------------------------------- /src/gestures/hooks/useWheel.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | 3 | import { type WheelEvent, WheelGesture } from '../controllers/WheelGesture'; 4 | import { useRecognizer } from './useRecognizer'; 5 | 6 | export function useWheel( 7 | refs: Window, 8 | onWheel: (e: WheelEvent & { index: 0 }) => void 9 | ): void; 10 | 11 | export function useWheel( 12 | refs: RefObject | Array>, 13 | onWheel: (e: WheelEvent & { index: number }) => void 14 | ): void; 15 | 16 | export function useWheel(refs: any, onWheel: any): void { 17 | return useRecognizer(WheelGesture, refs, onWheel); 18 | } 19 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "lib": [ 6 | "dom", 7 | "es2015" 8 | ], 9 | "downlevelIteration": true, 10 | "jsx": "react-jsx", 11 | "target": "es5", 12 | "allowJs": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmit": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "allowSyntheticDefaultImports": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useWheel/BasicSetup.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useWheel } from 'react-ui-animate'; 3 | 4 | const Example = () => { 5 | const [wheelPosition, setWheelPosition] = useState({ x: 0, y: 0 }); 6 | 7 | useWheel(window, function (event) { 8 | console.log('Wheel event:', event); 9 | setWheelPosition({ x: event.offset.x, y: event.offset.y }); 10 | }); 11 | 12 | return ( 13 |
14 |
15 | WHEEL POSITION: {wheelPosition.x}, {wheelPosition.y} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Example; 22 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | React UI Animate Examples 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useMove/BasicSetup.tsx: -------------------------------------------------------------------------------- 1 | import { animate, useValue, useMove } from 'react-ui-animate'; 2 | 3 | const Example = () => { 4 | const [pos, setPos] = useValue({ x: 0, y: 0 }); 5 | 6 | useMove(window, function ({ event }) { 7 | setPos({ x: event.clientX, y: event.clientY }); 8 | }); 9 | 10 | return ( 11 | 25 | ); 26 | }; 27 | 28 | export default Example; 29 | -------------------------------------------------------------------------------- /example/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-webpack5'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | 6 | addons: [ 7 | '@storybook/preset-create-react-app', 8 | '@storybook/addon-onboarding', 9 | '@storybook/addon-links', 10 | '@storybook/addon-essentials', 11 | '@chromatic-com/storybook', 12 | '@storybook/addon-interactions', 13 | ], 14 | 15 | framework: { 16 | name: '@storybook/react-webpack5', 17 | options: {}, 18 | }, 19 | 20 | staticDirs: ['../public'], 21 | 22 | docs: {}, 23 | 24 | typescript: { 25 | reactDocgen: 'react-docgen-typescript' 26 | } 27 | }; 28 | export default config; 29 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useDrag/BasicSetup.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useValue, animate, useDrag, withSpring } from 'react-ui-animate'; 3 | 4 | const Example = () => { 5 | const ref = useRef(null); 6 | const [translateX, setTranslateX] = useValue(0); 7 | 8 | useDrag(ref, ({ down, movement }) => { 9 | setTranslateX(down ? withSpring(movement.x) : withSpring(0)); 10 | }); 11 | 12 | return ( 13 | 23 | ); 24 | }; 25 | 26 | export default Example; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": ["es6", "dom", "es2016", "es2017", "es2019"], 7 | "downlevelIteration": true, 8 | "sourceMap": true, 9 | "allowJs": false, 10 | "jsx": "react-jsx", 11 | "declaration": true, 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "types": ["resize-observer-browser", "node", "@types/jest"] 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules", "dist", "example", "rollup.config.js"] 24 | } 25 | -------------------------------------------------------------------------------- /example/src/stories/animations/modules/Mount/BasicSetup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { animate, Mount } from 'react-ui-animate'; 3 | 4 | const Example: React.FC = () => { 5 | const [open, setOpen] = React.useState(true); 6 | 7 | return ( 8 | <> 9 | 10 | {(a) => ( 11 | 20 | )} 21 | 22 | 23 | 31 | 32 | ); 33 | }; 34 | 35 | export default Example; 36 | -------------------------------------------------------------------------------- /src/gestures/controllers/Gesture.ts: -------------------------------------------------------------------------------- 1 | type Listener = (event: E) => void; 2 | 3 | export abstract class Gesture { 4 | public static readonly VELOCITY_LIMIT = 20; 5 | 6 | private changeListeners = new Set>(); 7 | private endListeners = new Set>(); 8 | 9 | onChange(listener: Listener): this { 10 | this.changeListeners.add(listener); 11 | return this; 12 | } 13 | 14 | onEnd(listener: Listener): this { 15 | this.endListeners.add(listener); 16 | return this; 17 | } 18 | 19 | protected emitChange(event: E): void { 20 | this.changeListeners.forEach((fn) => fn(event)); 21 | } 22 | 23 | protected emitEnd(event: E): void { 24 | this.endListeners.forEach((fn) => fn(event)); 25 | } 26 | 27 | abstract attach(elements: HTMLElement | HTMLElement | Window): () => void; 28 | 29 | abstract cancel(): void; 30 | } 31 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useMount/BasicSetup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { animate, useMount } from 'react-ui-animate'; 3 | 4 | const Example: React.FC = () => { 5 | const [open, setOpen] = React.useState(true); 6 | const mounted = useMount(open); 7 | 8 | return ( 9 | <> 10 | {mounted( 11 | (a, m) => 12 | m && ( 13 | 22 | ) 23 | )} 24 | 25 | 33 | 34 | ); 35 | }; 36 | 37 | export default Example; 38 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useScroll/ScrollTrigger.tsx: -------------------------------------------------------------------------------- 1 | import { useScroll, animate } from 'react-ui-animate'; 2 | 3 | export default function IntersectionExample() { 4 | const { scrollYProgress } = useScroll(window); 5 | 6 | return ( 7 |
12 | 22 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 7 | sans-serif; 8 | } 9 | 10 | button { 11 | padding: 10px 16px; 12 | border: 2px solid #e1e1e1; 13 | font-weight: 500; 14 | border-radius: 4px; 15 | cursor: pointer; 16 | background-color: white; 17 | transition: background-color 0.3s ease; 18 | } 19 | 20 | button:hover { 21 | background-color: #f1f1f1; 22 | } 23 | 24 | button:active { 25 | background-color: #e1e1e1; 26 | } 27 | 28 | .button-group { 29 | display: flex; 30 | gap: 8px; 31 | } 32 | 33 | .m { 34 | margin: 10px; 35 | } 36 | 37 | .mt { 38 | margin-top: 10px; 39 | } 40 | 41 | .mb { 42 | margin-bottom: 10px; 43 | } 44 | 45 | .my { 46 | margin-top: 10px; 47 | margin-bottom: 10px; 48 | } 49 | 50 | .mx { 51 | margin-left: 10px; 52 | margin-right: 10px; 53 | } -------------------------------------------------------------------------------- /example/src/stories/animations/modules/Mount/Modifiers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { animate, Config, withSpring, Mount } from 'react-ui-animate'; 3 | 4 | const Example: React.FC = () => { 5 | const [open, setOpen] = React.useState(true); 6 | 7 | return ( 8 | <> 9 | 10 | {(animation) => ( 11 | 21 | )} 22 | 23 | 24 | 32 | 33 | ); 34 | }; 35 | 36 | export default Example; 37 | -------------------------------------------------------------------------------- /src/animation/Config.ts: -------------------------------------------------------------------------------- 1 | import { Easing } from '@raidipesh78/re-motion'; 2 | 3 | export const Config = { 4 | Timing: { 5 | BOUNCE: { duration: 500, easing: Easing.bounce }, 6 | EASE_IN: { duration: 500, easing: Easing.in(Easing.ease) }, 7 | EASE_OUT: { duration: 500, easing: Easing.out(Easing.ease) }, 8 | EASE_IN_OUT: { duration: 500, easing: Easing.inOut(Easing.ease) }, 9 | POWER1: { duration: 500, easing: Easing.bezier(0.17, 0.42, 0.51, 0.97) }, 10 | POWER2: { duration: 500, easing: Easing.bezier(0.07, 0.11, 0.13, 1) }, 11 | POWER3: { duration: 500, easing: Easing.bezier(0.09, 0.7, 0.16, 1.04) }, 12 | POWER4: { duration: 500, easing: Easing.bezier(0.05, 0.54, 0, 1.03) }, 13 | LINEAR: { duration: 500, easing: Easing.linear }, 14 | }, 15 | Spring: { 16 | ELASTIC: { mass: 1, damping: 18, stiffness: 250 }, 17 | EASE: { mass: 1, damping: 20, stiffness: 158 }, 18 | STIFF: { mass: 1, damping: 18, stiffness: 350 }, 19 | WOBBLE: { mass: 1, damping: 8, stiffness: 250 }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/hooks/events/useOutsideClick.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, RefObject, DependencyList } from 'react'; 2 | 3 | export function useOutsideClick( 4 | ref: RefObject, 5 | callback: (event: MouseEvent | TouchEvent) => void, 6 | deps: DependencyList = [] 7 | ): void { 8 | const cbRef = useRef(callback); 9 | 10 | useEffect(() => { 11 | cbRef.current = callback; 12 | }, [callback, ...deps]); 13 | 14 | useEffect(() => { 15 | function onClick(event: MouseEvent | TouchEvent) { 16 | const el = ref.current; 17 | const target = event.target as Node | null; 18 | 19 | if (!el || !target || !target.isConnected) return; 20 | if (!el.contains(target)) { 21 | cbRef.current(event); 22 | } 23 | } 24 | 25 | document.addEventListener('mousedown', onClick); 26 | document.addEventListener('touchstart', onClick); 27 | 28 | return () => { 29 | document.removeEventListener('mousedown', onClick); 30 | document.removeEventListener('touchstart', onClick); 31 | }; 32 | }, [ref]); 33 | } 34 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useDrag/ConditionalBinding.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { useValue, animate, useDrag, withSpring } from 'react-ui-animate'; 3 | 4 | const Example = () => { 5 | const ref = useRef(null); 6 | const [translateX, setTranslateX] = useValue(0); 7 | 8 | const [isEnabled, setIsEnabled] = useState(true); 9 | 10 | useDrag(ref, ({ down, movement }) => { 11 | if (isEnabled) { 12 | setTranslateX(down ? withSpring(movement.x) : withSpring(0)); 13 | } 14 | }); 15 | 16 | return ( 17 | <> 18 | 21 | 22 | 32 | 33 | ); 34 | }; 35 | 36 | export default Example; 37 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useMount/Modifiers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { animate, Config, useMount, withSpring } from 'react-ui-animate'; 3 | 4 | const Example: React.FC = () => { 5 | const [open, setOpen] = React.useState(true); 6 | const mounted = useMount(open, { 7 | enter: withSpring(1, Config.Spring.WOBBLE), 8 | }); 9 | 10 | return ( 11 | <> 12 | {mounted( 13 | (a, m) => 14 | m && ( 15 | 25 | ) 26 | )} 27 | 28 | 36 | 37 | ); 38 | }; 39 | 40 | export default Example; 41 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/String.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { animate, useValue, withSpring, withTiming } from 'react-ui-animate'; 3 | 4 | const Example: React.FC = () => { 5 | const [bg, setBg] = useValue('teal'); 6 | 7 | return ( 8 | <> 9 |
10 | 13 | 16 | 19 | 20 |
21 | 22 | 32 | 33 | ); 34 | }; 35 | 36 | export default Example; 37 | -------------------------------------------------------------------------------- /src/hooks/observers/useInView.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useState } from 'react'; 2 | 3 | export interface UseInViewOptions extends IntersectionObserverInit { 4 | once?: boolean; 5 | } 6 | 7 | export function useInView( 8 | ref: RefObject, 9 | options: UseInViewOptions = {} 10 | ) { 11 | const { root, rootMargin, threshold, once = false } = options; 12 | const [isInView, setIsInView] = useState(false); 13 | 14 | useEffect(() => { 15 | const el = ref.current; 16 | if (!el) return; 17 | 18 | const observer = new IntersectionObserver( 19 | ([entry]) => { 20 | if (entry.isIntersecting) { 21 | setIsInView(true); 22 | if (once) { 23 | observer.unobserve(entry.target); 24 | observer.disconnect(); 25 | } 26 | } else if (!once) { 27 | setIsInView(false); 28 | } 29 | }, 30 | { root: root ?? null, rootMargin, threshold } 31 | ); 32 | 33 | observer.observe(el); 34 | return () => observer.disconnect(); 35 | }, [ref, root, rootMargin, threshold, once]); 36 | 37 | return isInView; 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dipesh Rai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useMove/SingleElement.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { animate, useValue, useMove } from 'react-ui-animate'; 3 | 4 | const Example = () => { 5 | const ref = useRef(null); 6 | const [pos, setPos] = useValue({ x: 0, y: 0 }); 7 | 8 | useMove(ref, function ({ event }) { 9 | setPos({ x: event.clientX, y: event.clientY }); 10 | }); 11 | 12 | return ( 13 | <> 14 | 28 | 29 |
39 | 40 | ); 41 | }; 42 | 43 | export default Example; 44 | -------------------------------------------------------------------------------- /example/src/stories/examples/Svg.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useValue, useDrag, animate, withSpring } from 'react-ui-animate'; 3 | 4 | function Example() { 5 | const [dragX, setDragX] = useValue(0); 6 | const [followX, setFollowX] = useValue(0); 7 | const ref = useRef(null); 8 | 9 | useDrag(ref, ({ offset }) => { 10 | setDragX(offset.x); 11 | setFollowX(withSpring(offset.x)); 12 | }); 13 | 14 | return ( 15 |
16 | 24 | 25 | 26 | 36 | 37 |
38 | ); 39 | } 40 | 41 | export default Example; 42 | -------------------------------------------------------------------------------- /src/gestures/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import { MotionValue } from '@raidipesh78/re-motion'; 3 | 4 | import { type ScrollEvent, ScrollGesture } from '../controllers/ScrollGesture'; 5 | import { useRecognizer } from './useRecognizer'; 6 | import { 7 | useScrollProgress, 8 | type UseScrollProgressOptions, 9 | } from '../../hooks/observers/useScrollProgress'; 10 | 11 | export function useScroll( 12 | refs: Window | RefObject | RefObject[], 13 | onScroll: (e: ScrollEvent & { index: number }) => void 14 | ): void; 15 | 16 | export function useScroll( 17 | refs: Window | RefObject | RefObject[], 18 | options?: UseScrollProgressOptions 19 | ): { 20 | scrollYProgress: MotionValue; 21 | scrollXProgress: MotionValue; 22 | }; 23 | 24 | export function useScroll( 25 | refs: any, 26 | arg: any 27 | ): void | { 28 | scrollYProgress: MotionValue; 29 | scrollXProgress: MotionValue; 30 | } { 31 | if (typeof arg === 'function') { 32 | return useRecognizer(ScrollGesture, refs, arg); 33 | } 34 | 35 | return useScrollProgress(refs, arg); 36 | } 37 | -------------------------------------------------------------------------------- /src/animation/modules/Mount.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import type { MotionValue } from '@raidipesh78/re-motion'; 3 | 4 | import type { Primitive } from '../types'; 5 | import { 6 | useMount, 7 | type ConfigMulti, 8 | type ConfigSingle, 9 | } from '../hooks/useMount'; 10 | 11 | interface MountPropsSingle 12 | extends Partial> { 13 | state: boolean; 14 | children: (animation: MotionValue) => ReactNode; 15 | } 16 | 17 | interface MountPropsMulti> 18 | extends ConfigMulti { 19 | state: boolean; 20 | children: (animation: { [K in keyof I]: MotionValue }) => ReactNode; 21 | } 22 | 23 | export function Mount( 24 | props: MountPropsSingle 25 | ): JSX.Element; 26 | 27 | export function Mount>( 28 | props: MountPropsMulti 29 | ): JSX.Element; 30 | 31 | export function Mount({ 32 | state, 33 | children, 34 | ...config 35 | }: MountPropsSingle | MountPropsMulti) { 36 | const open = useMount(state, config); 37 | return ( 38 | <>{open((animation, mounted) => mounted && children(animation as any))} 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /example/src/stories/examples/Loop.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { 3 | animate, 4 | useValue, 5 | withTiming, 6 | withLoop, 7 | withSequence, 8 | } from 'react-ui-animate'; 9 | 10 | const Example = () => { 11 | const [translateX, setTranslateX] = useValue(-20); 12 | 13 | useEffect(() => { 14 | setTranslateX(0); 15 | }, [setTranslateX]); 16 | 17 | const handleClick = () => { 18 | setTranslateX( 19 | withSequence([ 20 | withTiming(-20, { duration: 50 }), 21 | withLoop( 22 | withSequence([ 23 | withTiming(20, { 24 | duration: 100, 25 | }), 26 | withTiming(-20, { 27 | duration: 100, 28 | }), 29 | ]), 30 | 5 31 | ), 32 | withTiming(0, { duration: 50 }), 33 | ]) 34 | ); 35 | }; 36 | 37 | return ( 38 | <> 39 | 49 | 50 | ); 51 | }; 52 | 53 | export default Example; 54 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useDrag/MultipleElements.tsx: -------------------------------------------------------------------------------- 1 | import { createRef, useMemo, useRef } from 'react'; 2 | import { animate, useValue, useDrag, withSpring } from 'react-ui-animate'; 3 | 4 | const Example = () => { 5 | const items = useRef(Array.from({ length: 5 }, () => 0)); 6 | const [positions, setPositions] = useValue(items.current); 7 | 8 | const refs = useMemo( 9 | () => Array.from({ length: 5 }, () => createRef()), 10 | [] 11 | ); 12 | 13 | useDrag(refs, function ({ down, movement, index }) { 14 | if (down) { 15 | const newPositions = [...items.current]; 16 | newPositions[index] = movement.x; 17 | setPositions(withSpring(newPositions)); 18 | } else { 19 | setPositions(withSpring(items.current)); 20 | } 21 | }); 22 | 23 | return ( 24 | <> 25 | {refs.map((r, i) => ( 26 | 38 | ))} 39 | 40 | ); 41 | }; 42 | 43 | export default Example; 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release & Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | dry_run: 7 | description: 'Set to false to actually tag & publish; true (default) skips both' 8 | required: false 9 | default: 'true' 10 | 11 | jobs: 12 | release: 13 | if: github.ref == 'refs/heads/main' 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '20' 29 | registry-url: 'https://registry.npmjs.org' 30 | always-auth: true 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | 34 | - name: Install dependencies 35 | run: npm ci 36 | 37 | - name: Build 38 | run: npm run build 39 | 40 | - name: Release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | run: | 45 | if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then 46 | npx semantic-release --dry-run 47 | else 48 | npx semantic-release 49 | fi 50 | -------------------------------------------------------------------------------- /example/src/stories/animations/modules/Mount/MultiValued.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | animate, 4 | withSequence, 5 | withTiming, 6 | withSpring, 7 | Mount, 8 | } from 'react-ui-animate'; 9 | 10 | const Example: React.FC = () => { 11 | const [open, setOpen] = React.useState(true); 12 | 13 | return ( 14 | <> 15 | 27 | {({ width, opacity, translateX, rotate }) => ( 28 | 39 | )} 40 | 41 | 42 | 50 | 51 | ); 52 | }; 53 | 54 | export default Example; 55 | -------------------------------------------------------------------------------- /.github/workflows/release-next.yml: -------------------------------------------------------------------------------- 1 | name: Release Next (Semantic Version + Dry Run) 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | dry_run: 7 | description: 'Set to false to actually tag & publish; true (default) skips both' 8 | required: false 9 | default: 'true' 10 | 11 | jobs: 12 | release: 13 | if: github.ref == 'refs/heads/next' 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '20' 29 | registry-url: 'https://registry.npmjs.org' 30 | always-auth: true 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | 34 | - name: Install dependencies 35 | run: npm ci 36 | 37 | - name: Build 38 | run: npm run build 39 | 40 | - name: Release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | run: | 45 | if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then 46 | npx semantic-release --dry-run 47 | else 48 | npx semantic-release 49 | fi 50 | -------------------------------------------------------------------------------- /src/animation/types.ts: -------------------------------------------------------------------------------- 1 | export type Primitive = number | string; 2 | 3 | export interface Callbacks { 4 | onStart?: () => void; 5 | onChange?: (v: number) => void; 6 | onComplete?: () => void; 7 | } 8 | 9 | export interface SpringOptions { 10 | stiffness?: number; 11 | damping?: number; 12 | mass?: number; 13 | } 14 | 15 | export interface TimingOptions { 16 | duration?: number; 17 | easing?: (t: number) => number; 18 | } 19 | 20 | export interface DecayOptions { 21 | velocity?: number; 22 | clamp?: [number, number]; 23 | } 24 | 25 | export interface SequenceOptions { 26 | animations?: Descriptor[]; 27 | } 28 | 29 | export interface DelayOptions { 30 | delay?: number; 31 | } 32 | 33 | export interface LoopOptions { 34 | iterations?: number; 35 | animation?: Descriptor; 36 | } 37 | 38 | export type DriverType = 39 | | 'spring' 40 | | 'timing' 41 | | 'decay' 42 | | 'delay' 43 | | 'sequence' 44 | | 'loop'; 45 | 46 | export interface Descriptor { 47 | type: DriverType; 48 | to?: Primitive | Primitive[] | Record; 49 | options?: SpringOptions & 50 | TimingOptions & 51 | DecayOptions & 52 | SequenceOptions & 53 | DelayOptions & 54 | LoopOptions & 55 | Callbacks; 56 | } 57 | 58 | export interface Controls { 59 | start(): void; 60 | pause(): void; 61 | resume(): void; 62 | cancel(): void; 63 | reset(): void; 64 | } 65 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useMount/MultiValued.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | animate, 4 | useMount, 5 | withSequence, 6 | withTiming, 7 | withSpring, 8 | } from 'react-ui-animate'; 9 | 10 | const Example: React.FC = () => { 11 | const [open, setOpen] = React.useState(true); 12 | 13 | const mountedValue = useMount(open, { 14 | from: { width: 200, opacity: 0, translateX: 0, rotate: 0 }, 15 | enter: withSequence([ 16 | withTiming({ translateX: 100, opacity: 1, rotate: 0 }), 17 | withSpring({ width: 300 }), 18 | ]), 19 | exit: withSpring({ 20 | translateX: 0, 21 | width: 200, 22 | }), 23 | }); 24 | 25 | return ( 26 | <> 27 | {mountedValue(({ width, opacity, translateX, rotate }, mounted) => { 28 | return ( 29 | mounted && ( 30 | 41 | ) 42 | ); 43 | })} 44 | 45 | 53 | 54 | ); 55 | }; 56 | 57 | export default Example; 58 | -------------------------------------------------------------------------------- /example/src/stories/gestures/hooks/useMove/MultipleElements.tsx: -------------------------------------------------------------------------------- 1 | import { createRef, useMemo, useState } from 'react'; 2 | import { animate, useValue, useMove } from 'react-ui-animate'; 3 | 4 | const Example = () => { 5 | const [open, setOpen] = useState(true); 6 | const [pos, setPos] = useValue({ x: 0, y: 0 }); 7 | 8 | const refs = useMemo( 9 | () => Array.from({ length: 5 }, () => createRef()), 10 | [] 11 | ); 12 | 13 | useMove(refs, function ({ event }) { 14 | if (open) { 15 | setPos({ x: event.clientX, y: event.clientY }); 16 | } 17 | }); 18 | 19 | return ( 20 | <> 21 | 27 | 28 | 42 | 43 | {refs.map((r, i) => ( 44 |
56 | ))} 57 | 58 | ); 59 | }; 60 | 61 | export default Example; 62 | -------------------------------------------------------------------------------- /example/src/stories/examples/InView.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { 3 | useInView, 4 | useValue, 5 | animate, 6 | withSpring, 7 | useScroll, 8 | } from 'react-ui-animate'; 9 | 10 | const COLORS = [ 11 | '#e4a3f1', 12 | '#0b7c2d', 13 | '#ff1a5b', 14 | '#3f2d9e', 15 | '#aacb47', 16 | '#d01e8f', 17 | '#5b9df4', 18 | '#f4c610', 19 | '#7e3a21', 20 | '#12d8c3', 21 | ]; 22 | 23 | const Card = ({ i, color }: { i: number; color: string }) => { 24 | const ref = useRef(null); 25 | const isInView = useInView(ref); 26 | const { scrollYProgress } = useScroll(window, { 27 | target: ref, 28 | offset: ['start center', 'start start'], 29 | }); 30 | const [progress, setProgress] = useValue(0); 31 | 32 | useEffect(() => { 33 | setProgress(withSpring(isInView ? 1 : 0)); 34 | }, [isInView, setProgress]); 35 | 36 | return ( 37 | 38 | 49 | 50 | ); 51 | }; 52 | 53 | const Example = () => { 54 | return ( 55 |
56 |

In View Example

57 |
58 | {COLORS.map((color, i) => ( 59 |
71 | 72 |
73 | ))} 74 |
75 | ); 76 | }; 77 | 78 | export default Example; 79 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.101", 12 | "@types/react": "^18.3.3", 13 | "@types/react-dom": "^18.3.0", 14 | "react": "../node_modules/react", 15 | "react-dom": "^18.3.1", 16 | "react-router-dom": "^6.25.1", 17 | "react-scripts": "^5.0.1", 18 | "react-ui-animate": "..", 19 | "typescript": "^4.9.5", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "npm run storybook", 24 | "build": "npm run build-storybook", 25 | "storybook": "storybook dev -p 6006", 26 | "build-storybook": "storybook build" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest", 32 | "plugin:storybook/recommended" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@chromatic-com/storybook": "^1.6.1", 49 | "@storybook/addon-essentials": "8.2.8", 50 | "@storybook/addon-interactions": "8.2.8", 51 | "@storybook/addon-links": "8.2.8", 52 | "@storybook/addon-onboarding": "8.2.8", 53 | "@storybook/blocks": "8.2.8", 54 | "@storybook/preset-create-react-app": "8.2.8", 55 | "@storybook/react": "8.2.8", 56 | "@storybook/react-webpack5": "8.2.8", 57 | "@storybook/test": "8.2.8", 58 | "eslint-plugin-storybook": "^0.8.0", 59 | "prop-types": "^15.8.1", 60 | "storybook": "8.2.8", 61 | "webpack": "^5.93.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/src/stories/examples/Stagger.tsx: -------------------------------------------------------------------------------- 1 | import { Children, useLayoutEffect, useState } from 'react'; 2 | import { 3 | useScroll, 4 | animate, 5 | useValue, 6 | withSequence, 7 | withDelay, 8 | withSpring, 9 | } from 'react-ui-animate'; 10 | 11 | const StaggerItem = ({ 12 | y, 13 | index, 14 | content, 15 | }: { 16 | y: number; 17 | index: number; 18 | content: string; 19 | }) => { 20 | const [top, setTop] = useValue(0); 21 | 22 | useLayoutEffect(() => { 23 | setTop(withSequence([withDelay(index * 50), withSpring(y)])); 24 | }, [y, index, setTop]); 25 | 26 | return ( 27 | 38 | {content} 39 | 40 | ); 41 | }; 42 | 43 | const Stagger = ({ y, children }: any) => { 44 | const childs = Children.toArray(children); 45 | 46 | return ( 47 |
48 | {childs.map((child: any, i) => ( 49 | 50 | ))} 51 |
52 | ); 53 | }; 54 | 55 | function Example() { 56 | const [y, setY] = useState(0); 57 | 58 | useScroll(window, ({ offset }) => { 59 | setY(offset.y); 60 | }); 61 | 62 | return ( 63 |
68 |
75 | 76 | Hello 👋 77 | I'm 78 | Dipesh 79 | Rai 80 | Welcome 81 | 82 |
83 |
84 | ); 85 | } 86 | 87 | export default Example; 88 | -------------------------------------------------------------------------------- /src/animation/descriptors.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './Config'; 2 | import { 3 | Callbacks, 4 | DecayOptions, 5 | Descriptor, 6 | SpringOptions, 7 | TimingOptions, 8 | } from './types'; 9 | 10 | export const withSpring = ( 11 | to: Descriptor['to'], 12 | opts?: SpringOptions & Callbacks 13 | ): Descriptor => ({ 14 | type: 'spring', 15 | to, 16 | options: { 17 | stiffness: opts?.stiffness ?? Config.Spring.EASE.stiffness, 18 | damping: opts?.damping ?? Config.Spring.EASE.damping, 19 | mass: opts?.mass ?? Config.Spring.EASE.mass, 20 | onStart: opts?.onStart, 21 | onChange: opts?.onChange, 22 | onComplete: opts?.onComplete, 23 | }, 24 | }); 25 | 26 | export const withTiming = ( 27 | to: Descriptor['to'], 28 | opts?: TimingOptions & Callbacks 29 | ): Descriptor => ({ 30 | type: 'timing', 31 | to, 32 | options: { 33 | duration: opts?.duration, 34 | easing: opts?.easing, 35 | onStart: opts?.onStart, 36 | onChange: opts?.onChange, 37 | onComplete: opts?.onComplete, 38 | }, 39 | }); 40 | 41 | export const withDecay = ( 42 | velocity: number, 43 | opts?: DecayOptions & Callbacks 44 | ): Descriptor => ({ 45 | type: 'decay', 46 | options: { 47 | velocity, 48 | clamp: opts?.clamp, 49 | onStart: opts?.onStart, 50 | onChange: opts?.onChange, 51 | onComplete: opts?.onComplete, 52 | }, 53 | }); 54 | 55 | export const withDelay = (ms: number): Descriptor => ({ 56 | type: 'delay', 57 | options: { delay: ms }, 58 | }); 59 | 60 | export const withSequence = ( 61 | animations: Descriptor[], 62 | opts?: Omit 63 | ): Descriptor => ({ 64 | type: 'sequence', 65 | options: { 66 | animations, 67 | onStart: opts?.onStart, 68 | onComplete: opts?.onComplete, 69 | }, 70 | }); 71 | 72 | export const withLoop = ( 73 | animation: Descriptor, 74 | iterations = Infinity, 75 | opts?: Omit 76 | ): Descriptor => ({ 77 | type: 'loop', 78 | options: { 79 | animation, 80 | iterations, 81 | onStart: opts?.onStart, 82 | onComplete: opts?.onComplete, 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # IntelliJ IDEA files 107 | .idea/ -------------------------------------------------------------------------------- /example/src/stories/examples/SnapPoints.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { 3 | animate, 4 | useDrag, 5 | useValue, 6 | snapTo, 7 | withSpring, 8 | } from 'react-ui-animate'; 9 | 10 | import '../../index.css'; 11 | 12 | const SNAP_COORDINATES = [ 13 | { x: 0, y: 0 }, 14 | { x: 200, y: 0 }, 15 | { x: 400, y: 0 }, 16 | { x: 600, y: 0 }, 17 | { x: 0, y: 200 }, 18 | { x: 200, y: 200 }, 19 | { x: 400, y: 200 }, 20 | { x: 600, y: 200 }, 21 | ]; 22 | 23 | function Example() { 24 | const [{ x, y }, setXY] = useValue({ x: 0, y: 0 }); 25 | const offset = useRef({ x: 0, y: 0 }); 26 | const ref = useRef(null); 27 | 28 | useDrag(ref, ({ movement, velocity, down }) => { 29 | if (!down) { 30 | offset.current = { 31 | x: movement.x + offset.current.x, 32 | y: movement.y + offset.current.y, 33 | }; 34 | 35 | const snapX = snapTo(offset.current.x, velocity.x, [0, 200, 400, 600]); 36 | const snapY = snapTo(offset.current.y, velocity.y, [0, 200, 400, 600]); 37 | 38 | setXY(withSpring({ x: snapX, y: snapY })); 39 | 40 | offset.current = { x: snapX, y: snapY }; 41 | } else { 42 | setXY({ 43 | x: movement.x + offset.current.x, 44 | y: movement.y + offset.current.y, 45 | }); 46 | } 47 | }); 48 | 49 | return ( 50 | <> 51 | 65 | 66 | {SNAP_COORDINATES.map((coord, index) => ( 67 |
80 | ))} 81 | 82 | ); 83 | } 84 | 85 | export default Example; 86 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/BasicSetup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | animate, 4 | useValue, 5 | withSpring, 6 | withTiming, 7 | withDecay, 8 | withSequence, 9 | withLoop, 10 | withDelay, 11 | } from 'react-ui-animate'; 12 | 13 | const Example: React.FC = () => { 14 | const [x, setX] = useValue(0); 15 | 16 | return ( 17 | <> 18 |
19 | 20 | 21 | 37 | 38 | 62 | 63 |
64 | 75 | 76 | ); 77 | }; 78 | 79 | export default Example; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ui-animate", 3 | "version": "5.0.0", 4 | "description": "React library for gestures and animation", 5 | "main": "dist/index.js", 6 | "peerDependencies": { 7 | "react": ">=16.8.0 || >=17.0.0 || >=18.0.0" 8 | }, 9 | "dependencies": { 10 | "@raidipesh78/re-motion": "^5.4.2" 11 | }, 12 | "devDependencies": { 13 | "@rollup/plugin-terser": "^0.4.4", 14 | "@semantic-release/commit-analyzer": "^13.0.1", 15 | "@semantic-release/git": "^10.0.1", 16 | "@semantic-release/github": "^11.0.3", 17 | "@semantic-release/npm": "^12.0.2", 18 | "@semantic-release/release-notes-generator": "^14.0.3", 19 | "@types/jest": "^29.5.12", 20 | "@types/node": "^20.14.9", 21 | "@types/react": "^18.3.3", 22 | "@types/react-dom": "^18.3.0", 23 | "@types/resize-observer-browser": "^0.1.11", 24 | "babel-core": "^5.8.38", 25 | "babel-runtime": "^6.26.0", 26 | "react": "^18.3.1", 27 | "react-dom": "^18.3.1", 28 | "rimraf": "^6.0.1", 29 | "rollup": "^4.18.0", 30 | "rollup-plugin-typescript2": "^0.36.0", 31 | "semantic-release": "^24.2.6", 32 | "typescript": "^5.5.2" 33 | }, 34 | "scripts": { 35 | "clean": "rimraf -rf dist", 36 | "build": "npm run clean && rollup -c", 37 | "start": "npm run clean && rollup -c -w", 38 | "start:dev": "cd example && npm start", 39 | "test": "echo \"Error: no test specified\" && exit 1", 40 | "version:minor": "npm version minor", 41 | "version:major": "npm version major", 42 | "version:patch": "npm version patch", 43 | "version:rc": "npm version prerelease --preid=rc", 44 | "version:prepatch": "npm version prepatch --preid=rc", 45 | "publish:next": "npm publish --tag next", 46 | "publish:latest": "npm publish --tag latest" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/dipeshrai123/react-ui-animate.git" 51 | }, 52 | "keywords": [ 53 | "gesture", 54 | "animation", 55 | "react-ui-animate" 56 | ], 57 | "author": "Dipesh Rai", 58 | "license": "MIT", 59 | "bugs": { 60 | "url": "https://github.com/dipeshrai123/react-ui-animate/issues" 61 | }, 62 | "homepage": "https://github.com/dipeshrai123/react-ui-animate#readme" 63 | } 64 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /src/gestures/hooks/useRecognizer.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef } from 'react'; 2 | 3 | interface GestureInstance { 4 | onChange(handler: (event: E) => void): this; 5 | onEnd(handler: (event: E) => void): this; 6 | attach(target: Window | HTMLElement): () => void; 7 | } 8 | 9 | interface GestureConstructor { 10 | new (config?: C): GestureInstance; 11 | } 12 | 13 | export function useRecognizer( 14 | GestureClass: GestureConstructor, 15 | refs: Window | RefObject | Array>, 16 | onEvent: (e: E & { index: number }) => void, 17 | config?: C 18 | ) { 19 | const handlerRef = useLatest(onEvent); 20 | const configRef = useLatest(config); 21 | 22 | if (refs === window) { 23 | const gestureRef = useRef>(); 24 | 25 | if (!gestureRef.current) { 26 | const g = new GestureClass(configRef.current); 27 | const handler = (e: E) => handlerRef.current({ ...e, index: 0 }); 28 | g.onChange(handler).onEnd(handler); 29 | gestureRef.current = g; 30 | } 31 | 32 | useEffect(() => { 33 | const cleanup = gestureRef.current!.attach(window); 34 | return () => { 35 | cleanup(); 36 | }; 37 | }, [refs]); 38 | 39 | return; 40 | } 41 | 42 | const list = Array.isArray(refs) ? refs : ([refs] as RefObject[]); 43 | const gesturesRef = useRef[]>([]); 44 | 45 | if (gesturesRef.current.length !== list.length) { 46 | gesturesRef.current = list.map((_, i) => { 47 | const g = new GestureClass(configRef.current); 48 | const handler = (e: E) => handlerRef.current({ ...e, index: i }); 49 | g.onChange(handler).onEnd(handler); 50 | return g; 51 | }); 52 | } 53 | 54 | useEffect(() => { 55 | const cleanups = list 56 | .map((r, i) => { 57 | const el = r.current; 58 | if (!el) return null; 59 | return gesturesRef.current[i].attach(el); 60 | }) 61 | .filter((fn): fn is () => void => !!fn); 62 | 63 | return () => cleanups.forEach((fn) => fn()); 64 | }, [list.map((r) => r.current)]); 65 | } 66 | 67 | function useLatest(value: T) { 68 | const ref = useRef(value); 69 | useEffect(() => { 70 | ref.current = value; 71 | }, [value]); 72 | return ref as React.MutableRefObject; 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function clamp(value: number, lowerbound: number, upperbound: number) { 2 | return Math.min(Math.max(value, lowerbound), upperbound); 3 | } 4 | 5 | function rubber2(distanceFromEdge: number, constant: number) { 6 | return Math.pow(distanceFromEdge, constant * 5); 7 | } 8 | 9 | function rubber(distanceFromEdge: number, dimension: number, constant: number) { 10 | if (dimension === 0 || Math.abs(dimension) === Infinity) 11 | return rubber2(distanceFromEdge, constant); 12 | return ( 13 | (distanceFromEdge * dimension * constant) / 14 | (dimension + constant * distanceFromEdge) 15 | ); 16 | } 17 | 18 | export function rubberClamp( 19 | value: number, 20 | lowerbound: number, 21 | upperbound: number, 22 | constant: number = 0.15 23 | ) { 24 | if (constant === 0) return clamp(value, lowerbound, upperbound); 25 | 26 | if (value < lowerbound) { 27 | return ( 28 | -rubber(lowerbound - value, upperbound - lowerbound, constant) + 29 | lowerbound 30 | ); 31 | } 32 | 33 | if (value > upperbound) { 34 | return ( 35 | +rubber(value - upperbound, upperbound - lowerbound, constant) + 36 | upperbound 37 | ); 38 | } 39 | 40 | return value; 41 | } 42 | 43 | export function snapTo( 44 | value: number, 45 | velocity: number, 46 | snapPoints: Array 47 | ): number { 48 | const finalValue = value + velocity * 0.2; 49 | const getDiff = (point: number) => Math.abs(point - finalValue); 50 | const deltas = snapPoints.map(getDiff); 51 | const minDelta = Math.min(...deltas); 52 | 53 | return snapPoints.reduce(function (acc, point) { 54 | if (getDiff(point) === minDelta) { 55 | return point; 56 | } else { 57 | return acc; 58 | } 59 | }); 60 | } 61 | 62 | export function move(array: Array, moveIndex: number, toIndex: number) { 63 | const item = array[moveIndex]; 64 | const length = array.length; 65 | const diff = moveIndex - toIndex; 66 | 67 | if (diff > 0) { 68 | return [ 69 | ...array.slice(0, toIndex), 70 | item, 71 | ...array.slice(toIndex, moveIndex), 72 | ...array.slice(moveIndex + 1, length), 73 | ]; 74 | } else if (diff < 0) { 75 | const targetIndex = toIndex + 1; 76 | return [ 77 | ...array.slice(0, moveIndex), 78 | ...array.slice(moveIndex + 1, targetIndex), 79 | item, 80 | ...array.slice(targetIndex, length), 81 | ]; 82 | } 83 | return array; 84 | } 85 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/AnimationControls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | animate, 4 | useValue, 5 | withSpring, 6 | withTiming, 7 | withDecay, 8 | withSequence, 9 | withLoop, 10 | withDelay, 11 | } from 'react-ui-animate'; 12 | 13 | const Example: React.FC = () => { 14 | const [x, setX, controls] = useValue(0); 15 | 16 | return ( 17 | <> 18 |
19 | 22 | 23 | 39 | 40 | 64 | 65 |
66 | 67 | 78 | 79 |
80 | 81 | 82 |
83 | 84 | ); 85 | }; 86 | 87 | export default Example; 88 | -------------------------------------------------------------------------------- /example/src/stories/examples/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | 3 | import { useMount, animate, useOutsideClick } from 'react-ui-animate'; 4 | 5 | const Modal = ({ 6 | visible, 7 | onClose, 8 | }: { 9 | visible: boolean; 10 | onClose: () => void; 11 | }) => { 12 | const ref = useRef(null); 13 | useOutsideClick(ref, onClose); 14 | 15 | const mount = useMount(visible); 16 | 17 | return ( 18 | <> 19 | {mount( 20 | (a, m) => 21 | m && ( 22 | 36 | 48 | 58 | 59 |
67 | MODAL CONTENT 68 |
69 |
70 |
71 | ) 72 | )} 73 | 74 | ); 75 | }; 76 | 77 | const Example = () => { 78 | const [modalOpen, setModalOpen] = useState(false); 79 | 80 | return ( 81 | <> 82 | 83 | setModalOpen(false)} /> 84 | 85 | ); 86 | }; 87 | 88 | export default Example; 89 | -------------------------------------------------------------------------------- /src/animation/hooks/useMount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef, useState } from 'react'; 2 | import { MotionValue } from '@raidipesh78/re-motion'; 3 | 4 | import { withSpring } from '../descriptors'; 5 | import { isDescriptor } from '../helpers'; 6 | import type { Primitive, Descriptor } from '../types'; 7 | import { useValue } from './useValue'; 8 | 9 | export type ConfigSingle = { 10 | from?: T; 11 | enter?: T | Descriptor; 12 | exit?: T | Descriptor; 13 | }; 14 | 15 | export type ConfigMulti> = { 16 | from: I; 17 | enter?: I | Descriptor; 18 | exit?: I | Descriptor; 19 | }; 20 | 21 | export function useMount( 22 | isOpen: boolean, 23 | config?: ConfigSingle 24 | ): ( 25 | fn: (value: MotionValue, mounted: boolean) => React.ReactNode 26 | ) => React.ReactNode; 27 | 28 | export function useMount>( 29 | isOpen: boolean, 30 | config: ConfigMulti 31 | ): ( 32 | fn: ( 33 | values: { [K in keyof I]: MotionValue }, 34 | mounted: boolean 35 | ) => React.ReactNode 36 | ) => React.ReactNode; 37 | 38 | export function useMount( 39 | isOpen: boolean, 40 | config: any = {} 41 | ): (fn: (values: any, mounted: boolean) => React.ReactNode) => React.ReactNode { 42 | const { from = 0, enter = 1, exit = 0 } = config as any; 43 | 44 | const [mounted, setMounted] = useState(isOpen); 45 | const initial = useRef(true); 46 | const [values, setValues] = useValue(from); 47 | 48 | const enterDesc = useMemo( 49 | () => (isDescriptor(enter) ? enter : withSpring(enter)), 50 | [enter] 51 | ); 52 | 53 | const exitDesc = useMemo(() => { 54 | if (isDescriptor(exit)) { 55 | return { 56 | ...exit, 57 | options: { 58 | ...exit.options, 59 | onComplete: () => { 60 | exit.options?.onComplete?.(); 61 | setMounted(false); 62 | }, 63 | }, 64 | }; 65 | } 66 | return withSpring(exit, { onComplete: () => setMounted(false) }); 67 | }, [exit]); 68 | 69 | useEffect(() => { 70 | if (initial.current) { 71 | initial.current = false; 72 | if (isOpen) { 73 | setMounted(true); 74 | queueMicrotask(() => setValues(enterDesc)); 75 | } 76 | return; 77 | } 78 | 79 | if (isOpen) { 80 | setMounted(true); 81 | queueMicrotask(() => setValues(enterDesc)); 82 | } else { 83 | queueMicrotask(() => setValues(exitDesc)); 84 | } 85 | }, [isOpen]); 86 | 87 | return (fn) => fn(values as any, mounted); 88 | } 89 | -------------------------------------------------------------------------------- /src/animation/drivers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | decay, 3 | MotionValue, 4 | spring, 5 | timing, 6 | parallel, 7 | delay, 8 | sequence, 9 | loop, 10 | } from '@raidipesh78/re-motion'; 11 | 12 | import { filterCallbackOptions } from './helpers'; 13 | import type { Primitive, Descriptor } from './types'; 14 | 15 | export function buildAnimation( 16 | mv: MotionValue, 17 | { type, to, options = {} }: Descriptor 18 | ): ReturnType { 19 | switch (type) { 20 | case 'spring': 21 | return spring(mv, to as Primitive, options); 22 | case 'timing': 23 | return timing(mv, to as Primitive, options); 24 | case 'decay': 25 | return decay(mv as MotionValue, options.velocity ?? 0, options); 26 | case 'delay': 27 | return delay(options.delay ?? 0); 28 | case 'sequence': { 29 | const animations = options.animations ?? []; 30 | const ctrls = animations.map((step) => buildAnimation(mv, step)); 31 | return sequence(ctrls, options); 32 | } 33 | case 'loop': { 34 | const innerDesc = options.animation; 35 | 36 | if (!innerDesc) { 37 | console.warn('[buildAnimation] loop missing `animation` descriptor'); 38 | return { start() {}, pause() {}, resume() {}, cancel() {}, reset() {} }; 39 | } 40 | 41 | const innerCtrl = 42 | innerDesc.type === 'sequence' 43 | ? sequence( 44 | (innerDesc.options?.animations ?? []).map((s) => 45 | buildAnimation(mv, s) 46 | ), 47 | innerDesc.options 48 | ) 49 | : buildAnimation(mv, innerDesc); 50 | 51 | return loop(innerCtrl, options.iterations ?? 0, options); 52 | } 53 | 54 | default: 55 | console.warn(`Unsupported animation type: ${type}`); 56 | return { start() {}, pause() {}, resume() {}, cancel() {}, reset() {} }; 57 | } 58 | } 59 | 60 | export function buildParallel( 61 | mvMap: Record>, 62 | step: Descriptor 63 | ) { 64 | const entries = Object.entries(mvMap).filter(([key]) => { 65 | return ( 66 | step.type === 'decay' || 67 | step.type === 'delay' || 68 | (step.to as Record)[key] !== undefined 69 | ); 70 | }); 71 | 72 | const ctrls = entries.map(([key, mv], idx) => 73 | buildAnimation(mv, { 74 | type: step.type, 75 | to: 76 | step.type === 'decay' || step.type === 'delay' 77 | ? (step.to as any) 78 | : (step.to as Record)[key], 79 | options: filterCallbackOptions(step.options, idx === 0), 80 | }) 81 | ); 82 | 83 | return parallel(ctrls); 84 | } 85 | -------------------------------------------------------------------------------- /src/gestures/controllers/WheelGesture.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../../utils'; 2 | import { Gesture } from './Gesture'; 3 | 4 | export interface WheelEvent { 5 | movement: { x: number; y: number }; 6 | offset: { x: number; y: number }; 7 | velocity: { x: number; y: number }; 8 | event: globalThis.WheelEvent; 9 | cancel?: () => void; 10 | } 11 | 12 | export class WheelGesture extends Gesture { 13 | private attachedEls = new Set(); 14 | 15 | private movement = { x: 0, y: 0 }; 16 | private offset = { x: 0, y: 0 }; 17 | private velocity = { x: 0, y: 0 }; 18 | 19 | private lastTime = 0; 20 | private endTimeout?: number; 21 | 22 | attach(elements: HTMLElement | HTMLElement[] | Window): () => void { 23 | const els = Array.isArray(elements) ? elements : [elements]; 24 | const wheel = this.onWheel.bind(this); 25 | 26 | els.forEach((el) => { 27 | this.attachedEls.add(el); 28 | el.addEventListener('wheel', wheel, { passive: false }); 29 | }); 30 | 31 | return () => { 32 | els.forEach((el) => { 33 | el.removeEventListener('wheel', wheel); 34 | this.attachedEls.delete(el); 35 | }); 36 | 37 | if (this.endTimeout != null) { 38 | clearTimeout(this.endTimeout); 39 | this.endTimeout = undefined; 40 | } 41 | }; 42 | } 43 | 44 | cancel(): void {} 45 | 46 | private onWheel(e: globalThis.WheelEvent) { 47 | e.preventDefault(); 48 | 49 | const now = e.timeStamp; 50 | const dt = Math.max((now - this.lastTime) / 1000, 1e-6); 51 | this.lastTime = now; 52 | 53 | const dx = e.deltaX; 54 | const dy = e.deltaY; 55 | 56 | this.movement = { x: dx, y: dy }; 57 | this.offset.x += dx; 58 | this.offset.y += dy; 59 | 60 | const rawX = dx / dt / 1000; 61 | const rawY = dy / dt / 1000; 62 | this.velocity = { 63 | x: clamp(rawX, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT), 64 | y: clamp(rawY, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT), 65 | }; 66 | 67 | this.emitChange({ 68 | movement: { ...this.movement }, 69 | offset: { ...this.offset }, 70 | velocity: { ...this.velocity }, 71 | event: e, 72 | cancel: () => { 73 | if (this.endTimeout != null) clearTimeout(this.endTimeout); 74 | }, 75 | }); 76 | 77 | if (this.endTimeout != null) clearTimeout(this.endTimeout); 78 | this.endTimeout = window.setTimeout(() => { 79 | this.emitEnd({ 80 | movement: { ...this.movement }, 81 | offset: { ...this.offset }, 82 | velocity: { ...this.velocity }, 83 | event: e, 84 | cancel: () => {}, 85 | }); 86 | }, 150); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/Array.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | animate, 4 | useValue, 5 | withSpring, 6 | withTiming, 7 | withDecay, 8 | withSequence, 9 | withLoop, 10 | } from 'react-ui-animate'; 11 | 12 | const Example: React.FC = () => { 13 | const [values, setValues] = useValue([0, 100, 200]); 14 | 15 | return ( 16 | <> 17 |
18 | 21 | 24 | 25 | 48 | 72 | 73 |
74 | 75 | {values.map((value, index) => ( 76 | 89 | ))} 90 | 91 | ); 92 | }; 93 | 94 | export default Example; 95 | -------------------------------------------------------------------------------- /example/src/stories/examples/Sorting.tsx: -------------------------------------------------------------------------------- 1 | import { createRef, useMemo, useRef, useState } from 'react'; 2 | import { 3 | animate, 4 | useDrag, 5 | clamp, 6 | move, 7 | useValue, 8 | withSpring, 9 | } from 'react-ui-animate'; 10 | 11 | const ITEMS = ['Please!', 'Can you', 'order', 'me ?']; 12 | 13 | const Example = () => { 14 | const originalIndex = useRef(ITEMS.map((_, i) => i)); 15 | const [animationY, setAnimationY] = useValue(ITEMS.map((_, i) => i * 70)); 16 | const [zIndex, setZIndex] = useValue(ITEMS.map(() => 0)); 17 | const boxes = useRef( 18 | ITEMS.map((_, i) => createRef()) 19 | ).current; 20 | 21 | useDrag(boxes, ({ index: i, down, movement }) => { 22 | const index = originalIndex.current.indexOf(i!); 23 | 24 | const newIndex = clamp( 25 | Math.round((index * 70 + movement.y) / 70), 26 | 0, 27 | ITEMS.length - 1 28 | ); 29 | const newOrder = move(originalIndex.current, index, newIndex); 30 | 31 | if (!down) { 32 | originalIndex.current = newOrder; 33 | } 34 | 35 | const a = []; 36 | const v = []; 37 | for (let j = 0; j < ITEMS.length; j++) { 38 | const isActive = down && j === i; 39 | a[j] = isActive ? index * 70 + movement.y : newOrder.indexOf(j) * 70; 40 | v[j] = isActive ? 1 : 0; 41 | } 42 | 43 | setAnimationY(withSpring(a)); 44 | setZIndex(v); 45 | }); 46 | 47 | const boxShadows = useMemo( 48 | () => 49 | zIndex.map((z) => 50 | z.to((v) => (v === 1 ? '0 8px 16px rgba(0,0,0,0.12)' : 'none')) 51 | ), 52 | [zIndex] 53 | ); 54 | 55 | return ( 56 | <> 57 |
58 | {animationY.map((y, i) => ( 59 | 84 | {ITEMS[i]} 85 | 86 | ))} 87 |
88 | 89 | ); 90 | }; 91 | 92 | export default Example; 93 | -------------------------------------------------------------------------------- /example/src/stories/examples/Ripple.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent, useLayoutEffect, useState } from 'react'; 2 | import { animate, useValue, withTiming } from 'react-ui-animate'; 3 | 4 | import '../../index.css'; 5 | 6 | const RIPPLE_SIZE = 50; 7 | 8 | function Ripple({ 9 | id, 10 | x, 11 | y, 12 | onRemove, 13 | }: { 14 | id: number; 15 | x: number; 16 | y: number; 17 | onRemove: (id: number) => void; 18 | }) { 19 | const [animation, setAnimation] = useValue(0); 20 | 21 | useLayoutEffect(() => { 22 | setAnimation( 23 | withTiming(1, { 24 | duration: 800, 25 | onComplete: () => { 26 | onRemove(id); 27 | }, 28 | }) 29 | ); 30 | }, [id, setAnimation, onRemove]); 31 | 32 | return ( 33 | 47 | ); 48 | } 49 | 50 | let _uniqueId = 0; 51 | 52 | function Example() { 53 | const [ripples, setRipples] = useState< 54 | Array<{ id: number; x: number; y: number }> 55 | >([]); 56 | 57 | const addRipple = ({ 58 | clientX, 59 | clientY, 60 | currentTarget, 61 | }: MouseEvent) => { 62 | const { left, top } = currentTarget.getBoundingClientRect(); 63 | 64 | setRipples((previousRipples) => [ 65 | ...previousRipples, 66 | { 67 | id: _uniqueId++, 68 | x: clientX - left - RIPPLE_SIZE / 2, 69 | y: clientY - top - RIPPLE_SIZE / 2, 70 | }, 71 | ]); 72 | }; 73 | 74 | const removeRipple = (id: number) => { 75 | setRipples((prevRipples) => 76 | prevRipples.filter((ripple) => ripple.id !== id) 77 | ); 78 | }; 79 | 80 | return ( 81 | 105 | ); 106 | } 107 | 108 | export default Example; 109 | -------------------------------------------------------------------------------- /src/gestures/controllers/ScrollGesture.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../../utils'; 2 | import { Gesture } from './Gesture'; 3 | 4 | export interface ScrollEvent { 5 | movement: { x: number; y: number }; 6 | offset: { x: number; y: number }; 7 | velocity: { x: number; y: number }; 8 | event: Event; 9 | cancel?: () => void; 10 | } 11 | 12 | export class ScrollGesture extends Gesture { 13 | private attachedEls = new Set(); 14 | 15 | private movement = { x: 0, y: 0 }; 16 | private offset = { x: 0, y: 0 }; 17 | private velocity = { x: 0, y: 0 }; 18 | 19 | private prevScroll = { x: 0, y: 0 }; 20 | private lastTime = 0; 21 | private endTimeout?: number; 22 | 23 | attach(elements: HTMLElement | HTMLElement[] | Window): () => void { 24 | const els = Array.isArray(elements) ? elements : [elements]; 25 | const scroll = this.onScroll.bind(this); 26 | 27 | els.forEach((el) => { 28 | this.attachedEls.add(el); 29 | el.addEventListener('scroll', scroll, { passive: true }); 30 | }); 31 | 32 | return () => { 33 | els.forEach((el) => { 34 | el.removeEventListener('scroll', scroll); 35 | this.attachedEls.delete(el); 36 | }); 37 | 38 | if (this.endTimeout != null) { 39 | clearTimeout(this.endTimeout); 40 | this.endTimeout = undefined; 41 | } 42 | }; 43 | } 44 | 45 | cancel(): void {} 46 | 47 | private onScroll(e: Event) { 48 | const now = Date.now(); 49 | const dt = Math.max((now - this.lastTime) / 1000, 1e-6); 50 | this.lastTime = now; 51 | 52 | const tgt = e.currentTarget as HTMLElement | Window; 53 | const x = tgt instanceof HTMLElement ? tgt.scrollLeft : window.scrollX; 54 | const y = tgt instanceof HTMLElement ? tgt.scrollTop : window.scrollY; 55 | 56 | const dx = x - this.prevScroll.x; 57 | const dy = y - this.prevScroll.y; 58 | this.prevScroll = { x, y }; 59 | 60 | this.movement = { x: dx, y: dy }; 61 | this.offset = { x, y }; 62 | 63 | const rawX = dx / dt / 1000; 64 | const rawY = dy / dt / 1000; 65 | this.velocity = { 66 | x: clamp(rawX, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT), 67 | y: clamp(rawY, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT), 68 | }; 69 | 70 | this.emitChange({ 71 | movement: { ...this.movement }, 72 | offset: { ...this.offset }, 73 | velocity: { ...this.velocity }, 74 | event: e, 75 | cancel: () => { 76 | if (this.endTimeout != null) clearTimeout(this.endTimeout); 77 | }, 78 | }); 79 | 80 | if (this.endTimeout != null) clearTimeout(this.endTimeout); 81 | this.endTimeout = window.setTimeout(() => { 82 | this.emitEnd({ 83 | movement: { ...this.movement }, 84 | offset: { ...this.offset }, 85 | velocity: { ...this.velocity }, 86 | event: e, 87 | cancel: () => {}, 88 | }); 89 | }, 150); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /example/src/stories/examples/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { 3 | animate, 4 | useMount, 5 | withSpring, 6 | withSequence, 7 | withTiming, 8 | } from 'react-ui-animate'; 9 | 10 | const Toast = ({ id, onEnd }: any) => { 11 | const [visible, setVisible] = useState(true); 12 | 13 | const mount = useMount(visible, { 14 | from: { opacity: 0, height: 0, progress: 0 }, 15 | enter: withSequence([ 16 | withSpring( 17 | { 18 | opacity: 1, 19 | height: 100, 20 | }, 21 | { damping: 14 } 22 | ), 23 | withTiming( 24 | { progress: 1 }, 25 | { duration: 2000, onComplete: () => setVisible(false) } 26 | ), 27 | ]), 28 | exit: withSpring({ opacity: 0 }, { onComplete: () => onEnd(id) }), 29 | }); 30 | 31 | return ( 32 | <> 33 | {mount( 34 | ({ height, opacity, progress }, m) => 35 | m && ( 36 | 47 | 58 | 59 | ) 60 | )} 61 | 62 | ); 63 | }; 64 | 65 | var uniqueId = 0; 66 | 67 | const Example = () => { 68 | const [elements, setElements] = useState<{ id: number }[]>([]); 69 | 70 | const generateToast = () => { 71 | setElements((prev) => [...prev, { id: uniqueId++ }]); 72 | }; 73 | 74 | const handleEnd = useCallback((id: number) => { 75 | setElements((els) => els.filter((e) => e.id !== id)); 76 | }, []); 77 | 78 | return ( 79 | <> 80 | 92 | 93 |
103 | {elements.map((e) => { 104 | return ; 105 | })} 106 |
107 | 108 | ); 109 | }; 110 | 111 | export default Example; 112 | -------------------------------------------------------------------------------- /src/gestures/controllers/MoveGesture.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../../utils'; 2 | import { Gesture } from './Gesture'; 3 | 4 | export interface MoveEvent { 5 | movement: { x: number; y: number }; 6 | offset: { x: number; y: number }; 7 | velocity: { x: number; y: number }; 8 | event: PointerEvent; 9 | cancel?: () => void; 10 | } 11 | 12 | export class MoveGesture extends Gesture { 13 | private attachedEls = new Set(); 14 | 15 | private prev = { x: 0, y: 0 }; 16 | private lastTime = 0; 17 | 18 | private movement = { x: 0, y: 0 }; 19 | private offset = { x: 0, y: 0 }; 20 | private velocity = { x: 0, y: 0 }; 21 | private startPos: { x: number; y: number } | null = null; 22 | 23 | attach(elements: HTMLElement | HTMLElement[] | Window): () => void { 24 | const els = Array.isArray(elements) ? elements : [elements]; 25 | const move = this.onMove.bind(this); 26 | const leave = this.onLeave.bind(this); 27 | 28 | els.forEach((el) => { 29 | this.attachedEls.add(el); 30 | el.addEventListener('pointermove', move, { passive: false }); 31 | el.addEventListener('pointerleave', leave); 32 | }); 33 | 34 | return () => { 35 | els.forEach((el) => { 36 | el.removeEventListener('pointermove', move); 37 | el.removeEventListener('pointerleave', leave); 38 | this.attachedEls.delete(el); 39 | }); 40 | }; 41 | } 42 | 43 | cancel(): void {} 44 | 45 | private onMove(e: PointerEvent) { 46 | const now = e.timeStamp; 47 | 48 | if (this.startPos === null) { 49 | this.startPos = { x: e.clientX, y: e.clientY }; 50 | this.prev = { x: e.clientX, y: e.clientY }; 51 | this.lastTime = now; 52 | } 53 | 54 | const dt = Math.max((now - this.lastTime) / 1000, 1e-6); 55 | this.lastTime = now; 56 | 57 | const dx = e.clientX - this.prev.x; 58 | const dy = e.clientY - this.prev.y; 59 | this.prev = { x: e.clientX, y: e.clientY }; 60 | 61 | this.movement = { 62 | x: e.clientX - this.startPos.x, 63 | y: e.clientY - this.startPos.y, 64 | }; 65 | 66 | const tgt = e.currentTarget as HTMLElement | Window; 67 | const rect = 68 | tgt instanceof HTMLElement 69 | ? tgt.getBoundingClientRect() 70 | : { left: 0, top: 0 }; 71 | 72 | this.offset = { 73 | x: e.clientX - rect.left, 74 | y: e.clientY - rect.top, 75 | }; 76 | 77 | const rawVx = dx / dt / 1000; 78 | const rawVy = dy / dt / 1000; 79 | this.velocity = { 80 | x: clamp(rawVx, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT), 81 | y: clamp(rawVy, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT), 82 | }; 83 | 84 | this.emitChange({ 85 | movement: { ...this.movement }, 86 | offset: { ...this.offset }, 87 | velocity: { ...this.velocity }, 88 | event: e, 89 | cancel: () => this.onLeave(e), 90 | }); 91 | } 92 | 93 | private onLeave(e: PointerEvent) { 94 | this.emitEnd({ 95 | movement: { ...this.movement }, 96 | offset: { ...this.offset }, 97 | velocity: { ...this.velocity }, 98 | event: e, 99 | cancel: () => {}, 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /example/src/stories/examples/Slider.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { 3 | animate, 4 | useValue, 5 | useDrag, 6 | clamp, 7 | withSpring, 8 | to, 9 | } from 'react-ui-animate'; 10 | 11 | export default function Example() { 12 | const ref = useRef(null); 13 | const balloonRef = useRef(null); 14 | const offsetLeft = useRef(0); 15 | const [left, setLeft] = useValue(0); 16 | const [isDown, setIsDown] = useValue(0); 17 | const [balloonLeft, setBalloonLeft] = useValue(0); 18 | const [velocity, setVelocity] = useValue(0); 19 | 20 | useDrag(ref, ({ movement, down, velocity }) => { 21 | setIsDown(withSpring(down ? 1 : 0)); 22 | setVelocity(velocity.x); 23 | 24 | const ballX = clamp(offsetLeft.current + movement.x, 0, 190); 25 | if (down) { 26 | setLeft(ballX); 27 | setBalloonLeft(withSpring(ballX)); 28 | } else { 29 | offsetLeft.current = ballX; 30 | } 31 | 32 | if (balloonRef.current) { 33 | balloonRef.current.innerHTML = `${Number( 34 | to(ballX, [0, 190], [0, 100]) 35 | ).toFixed(0)}%`; 36 | } 37 | }); 38 | 39 | return ( 40 |
49 |
56 | 77 | 78 |
79 | 95 | 96 |
107 |
108 |
109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /example/src/stories/examples/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { animate, Mount, withSpring } from 'react-ui-animate'; 3 | 4 | const TodoListItem = ({ 5 | text, 6 | onRemove, 7 | }: { 8 | text: string; 9 | onRemove: () => void; 10 | }) => { 11 | const [visible, setVisible] = useState(false); 12 | 13 | useEffect(() => { 14 | setVisible(true); 15 | }, []); 16 | 17 | return ( 18 | onRemove() })} 21 | > 22 | {(opacity) => ( 23 | 34 | 47 | {text} 48 | 49 | 61 | 62 | )} 63 | 64 | ); 65 | }; 66 | 67 | var _uniqueId = 0; 68 | 69 | const Example = () => { 70 | const [text, setText] = useState(''); 71 | const [todos, setTodos] = useState<{ id: number; text: string }[]>([]); 72 | 73 | const saveTodo = () => { 74 | setTodos((prev) => [...prev, { id: _uniqueId++, text }]); 75 | setText(''); 76 | }; 77 | 78 | return ( 79 |
80 | setText(e.target.value)} 90 | /> 91 | 92 | 98 | 99 |
108 | {todos 109 | .map(({ id, text }) => ( 110 | 114 | setTodos((prev) => prev.filter((p) => p.id !== id)) 115 | } 116 | /> 117 | )) 118 | .reverse()} 119 |
120 |
121 | ); 122 | }; 123 | 124 | export default Example; 125 | -------------------------------------------------------------------------------- /example/src/stories/animations/hooks/useValue/Object.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | animate, 4 | useValue, 5 | withSpring, 6 | withTiming, 7 | withDecay, 8 | withSequence, 9 | withLoop, 10 | withDelay, 11 | } from 'react-ui-animate'; 12 | 13 | const Example: React.FC = () => { 14 | const [obj, setObj] = useValue({ x: 0, y: 0, width: 100, height: 100 }); 15 | 16 | return ( 17 | <> 18 |
19 | 34 | 49 | 61 | 86 | 111 | 114 |
115 | 116 | 128 | 129 | ); 130 | }; 131 | 132 | export default Example; 133 | -------------------------------------------------------------------------------- /example/src/stories/examples/SharedElement.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { 3 | clamp, 4 | animate, 5 | useDrag, 6 | useValue, 7 | withTiming, 8 | withSpring, 9 | withSequence, 10 | } from 'react-ui-animate'; 11 | 12 | const BOX_SIZE = 200; 13 | 14 | const IMAGES = [ 15 | 'https://images.unsplash.com/photo-1502082553048-f009c37129b9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80', 16 | 'https://images.unsplash.com/photo-1475924156734-496f6cac6ec1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80', 17 | 'https://images.unsplash.com/photo-1470770903676-69b98201ea1c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80', 18 | 'https://images.unsplash.com/photo-1444464666168-49d633b86797?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1469&q=80', 19 | ]; 20 | 21 | function Example() { 22 | const ref = useRef(null); 23 | const [activeIndex, setActiveIndex] = React.useState(null); 24 | 25 | const [{ left, top, width, height, translateY }, setValue] = useValue({ 26 | left: 0, 27 | top: 0, 28 | width: 0, 29 | height: 0, 30 | translateY: 0, 31 | }); 32 | 33 | useDrag(ref, ({ down, movement }) => { 34 | setValue( 35 | withSpring({ 36 | translateY: down ? clamp(movement.y, 0, 300) : 0, 37 | }) 38 | ); 39 | 40 | if (movement.y > 200 && !down) { 41 | closeSharedElement(); 42 | } 43 | }); 44 | 45 | React.useLayoutEffect(() => { 46 | if (activeIndex !== null) { 47 | const box = document.getElementById(`box-${activeIndex}`)!; 48 | const { left, top, width, height } = box.getBoundingClientRect(); 49 | 50 | setValue( 51 | withSequence([ 52 | withTiming({ left, top, width, height }, { duration: 0 }), 53 | withSpring( 54 | { 55 | left: 0, 56 | top: 0, 57 | width: window.innerWidth, 58 | height: window.innerHeight, 59 | }, 60 | { 61 | damping: 14, 62 | } 63 | ), 64 | ]) 65 | ); 66 | } 67 | }, [activeIndex, setValue]); 68 | 69 | const closeSharedElement = () => { 70 | if (activeIndex !== null) { 71 | const activeBox = document.getElementById(`box-${activeIndex}`); 72 | const clientRect = activeBox!.getBoundingClientRect(); 73 | 74 | setValue( 75 | withSpring( 76 | { 77 | left: clientRect.left, 78 | top: clientRect.top, 79 | width: clientRect.width, 80 | height: clientRect.height, 81 | translateY: 0, 82 | }, 83 | { 84 | onComplete: () => setActiveIndex(null), 85 | damping: 14, 86 | } 87 | ) 88 | ); 89 | } 90 | }; 91 | 92 | return ( 93 | <> 94 |
101 | {IMAGES.map((image, index) => { 102 | const imageStyle = 103 | activeIndex === index 104 | ? { 105 | backgroundColor: 'white', 106 | } 107 | : { 108 | backgroundImage: `url(${image})`, 109 | backgroundSize: 'cover', 110 | }; 111 | 112 | return ( 113 |
setActiveIndex(index)} 124 | /> 125 | ); 126 | })} 127 |
128 | 129 | {activeIndex !== null && ( 130 | 140 | 159 | Pull Down 160 | 161 | 162 | )} 163 | 164 | ); 165 | } 166 | 167 | export default Example; 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React UI Animate 2 | 3 | [![npm version](https://badge.fury.io/js/react-ui-animate.svg)](https://badge.fury.io/js/react-ui-animate) 4 | 5 | > Create smooth animations and interactive gestures in React applications effortlessly. 6 | 7 | ### Install 8 | 9 | You can install `react-ui-animate` via `npm` or `yarn`: 10 | 11 | ```sh 12 | npm install react-ui-animate 13 | ``` 14 | 15 | ```sh 16 | yarn add react-ui-animate 17 | ``` 18 | 19 | --- 20 | 21 | ## Getting Started 22 | 23 | The `react-ui-animate` library provides a straightforward way to add animations and gestures to your React components. Below are some common use cases. 24 | 25 | ### 1. useValue 26 | 27 | Use `useValue` to initialize and update an animated value. 28 | 29 | ```tsx 30 | import React from 'react'; 31 | import { 32 | animate, 33 | useValue, 34 | withSpring, 35 | withTiming, 36 | withSequence, 37 | } from 'react-ui-animate'; 38 | 39 | export const UseValue: React.FC = () => { 40 | const [width, setWidth] = useValue(100); 41 | 42 | return ( 43 | <> 44 | 51 | 58 | 65 | 66 | 75 | 76 | ); 77 | }; 78 | ``` 79 | 80 | ### 2. useMount 81 | 82 | Use `useMount` to animate component mount and unmount transitions. 83 | 84 | ```tsx 85 | import React from 'react'; 86 | import { 87 | animate, 88 | useMount, 89 | withDecay, 90 | withSequence, 91 | withSpring, 92 | withTiming, 93 | } from 'react-ui-animate'; 94 | 95 | export const UseMount: React.FC = () => { 96 | const [open, setOpen] = React.useState(true); 97 | const mounted = useMount(open, { from: 0, enter: 1, exit: 0 }); 98 | 99 | return ( 100 | <> 101 | {mounted( 102 | (animation, isMounted) => 103 | isMounted && ( 104 | 112 | ) 113 | )} 114 | 115 | 116 | 117 | ); 118 | }; 119 | ``` 120 | 121 | ### 3. Interpolation 122 | 123 | Interpolate values for complex mappings like color transitions or movement. 124 | 125 | ```tsx 126 | import React, { useLayoutEffect, useState } from 'react'; 127 | import { animate, useValue, withSpring } from 'react-ui-animate'; 128 | 129 | export const Interpolation: React.FC = () => { 130 | const [open, setOpen] = useState(false); 131 | const [x, setX] = useValue(0); 132 | 133 | useLayoutEffect(() => { 134 | setX(withSpring(open ? 500 : 0)); 135 | }, [open, setX]); 136 | 137 | return ( 138 | <> 139 | 147 | 148 | 149 | 150 | ); 151 | }; 152 | ``` 153 | 154 | --- 155 | 156 | ## API Overview 157 | 158 | - **`useValue(initial)`**: Initializes an animated value. 159 | - **`animate`**: JSX wrapper for animatable elements (`animate.div`, `animate.span`, etc.). 160 | - **Modifiers**: `withSpring`, `withTiming`, `withDecay`, `withSequence` — functions to define animation behavior. 161 | - **`useMount(state, config)`**: Manages mount/unmount transitions. `config` includes `from`, `enter`, and `exit` values. 162 | 163 | ## Gestures 164 | 165 | `react-ui-animate` also provides hooks for handling gestures: 166 | 167 | - `useDrag` 168 | - `useMove` 169 | - `useScroll` 170 | - `useWheel` 171 | 172 | **Example: `useDrag`** 173 | 174 | ```tsx 175 | import React from 'react'; 176 | import { useValue, animate, useDrag, withSpring } from 'react-ui-animate'; 177 | 178 | export const Draggable: React.FC = () => { 179 | const ref = useRef(null); 180 | const [translateX, setTranslateX] = useValue(0); 181 | 182 | useDrag(ref, ({ down, movement }) => { 183 | setTranslateX(down ? movement.x : withSpring(0)); 184 | }); 185 | 186 | return ( 187 | 196 | ); 197 | }; 198 | ``` 199 | 200 | ## Documentation 201 | 202 | For detailed documentation and examples, visit the official [react-ui-animate documentation](https://react-ui-animate.js.org/). 203 | 204 | ## License 205 | 206 | This library is licensed under the MIT License. 207 | -------------------------------------------------------------------------------- /src/gestures/controllers/DragGesture.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../../utils'; 2 | import { Gesture } from './Gesture'; 3 | 4 | export interface DragEvent { 5 | down: boolean; 6 | movement: { x: number; y: number }; 7 | offset: { x: number; y: number }; 8 | velocity: { x: number; y: number }; 9 | event: PointerEvent; 10 | cancel: () => void; 11 | } 12 | 13 | export interface DragConfig { 14 | threshold?: number; 15 | axis?: 'x' | 'y'; 16 | initial?: () => { x: number; y: number }; 17 | } 18 | 19 | export class DragGesture extends Gesture { 20 | private config: DragConfig; 21 | private prev = { x: 0, y: 0 }; 22 | private lastTime = 0; 23 | 24 | private movement = { x: 0, y: 0 }; 25 | private velocity = { x: 0, y: 0 }; 26 | private start = { x: 0, y: 0 }; 27 | private offset = { x: 0, y: 0 }; 28 | 29 | private pointerCaptured = false; 30 | private activePointerId: number | null = null; 31 | private attachedEls = new Set(); 32 | private activeEl: HTMLElement | null = null; 33 | private pointerDownPos = { x: 0, y: 0 }; 34 | private thresholdPassed = false; 35 | 36 | constructor(config: DragConfig = {}) { 37 | super(); 38 | this.config = config; 39 | } 40 | 41 | attach(elements: HTMLElement | HTMLElement[] | Window): () => void { 42 | if (elements === window) return () => {}; 43 | 44 | const els = Array.isArray(elements) ? elements : [elements as HTMLElement]; 45 | const down = this.onDown.bind(this); 46 | const move = this.onMove.bind(this); 47 | const up = this.onUp.bind(this); 48 | 49 | els.forEach((el) => { 50 | this.attachedEls.add(el); 51 | el.addEventListener('pointerdown', down, { passive: false }); 52 | }); 53 | 54 | window.addEventListener('pointermove', move, { passive: false }); 55 | window.addEventListener('pointerup', up); 56 | window.addEventListener('pointercancel', up); 57 | 58 | return () => { 59 | els.forEach((el) => { 60 | el.removeEventListener('pointerdown', down); 61 | this.attachedEls.delete(el); 62 | }); 63 | 64 | window.removeEventListener('pointermove', move); 65 | window.removeEventListener('pointerup', up); 66 | window.removeEventListener('pointercancel', up); 67 | }; 68 | } 69 | 70 | private onDown(e: PointerEvent) { 71 | if (e.button !== 0) return; 72 | 73 | const target = e.currentTarget as HTMLElement; 74 | if (!this.attachedEls.has(target)) return; 75 | 76 | this.activeEl = target; 77 | this.activePointerId = e.pointerId; 78 | this.pointerCaptured = false; 79 | 80 | this.start = 81 | this.thresholdPassed === false && this.start.x === 0 && this.start.y === 0 82 | ? this.config.initial?.() ?? { x: 0, y: 0 } 83 | : { ...this.offset }; 84 | this.offset = { ...this.start }; 85 | this.movement = { x: 0, y: 0 }; 86 | this.velocity = { x: 0, y: 0 }; 87 | 88 | this.pointerDownPos = { x: e.clientX, y: e.clientY }; 89 | this.thresholdPassed = false; 90 | this.prev = { x: e.clientX, y: e.clientY }; 91 | this.lastTime = e.timeStamp; 92 | 93 | this.emitChange({ 94 | down: true, 95 | movement: { x: 0, y: 0 }, 96 | offset: { ...this.offset }, 97 | velocity: { x: 0, y: 0 }, 98 | event: e, 99 | cancel: this.cancel.bind(this), 100 | }); 101 | } 102 | 103 | private onMove(e: PointerEvent) { 104 | if (this.activePointerId !== e.pointerId || !this.activeEl) return; 105 | 106 | const threshold = this.config.threshold ?? 0; 107 | if (!this.thresholdPassed) { 108 | const dxTotal = e.clientX - this.pointerDownPos.x; 109 | const dyTotal = e.clientY - this.pointerDownPos.y; 110 | const dist = Math.hypot(dxTotal, dyTotal); 111 | if (dist < threshold) return; 112 | this.thresholdPassed = true; 113 | 114 | this.activeEl.setPointerCapture(e.pointerId); 115 | this.pointerCaptured = true; 116 | } 117 | 118 | if (this.pointerCaptured) { 119 | e.preventDefault(); 120 | } 121 | 122 | const dt = Math.max((e.timeStamp - this.lastTime) / 1000, 1e-6); 123 | this.lastTime = e.timeStamp; 124 | const dx = e.clientX - this.prev.x; 125 | const dy = e.clientY - this.prev.y; 126 | const rawX = dx / dt / 1000; 127 | const rawY = dy / dt / 1000; 128 | this.velocity = { 129 | x: clamp(rawX, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT), 130 | y: clamp(rawY, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT), 131 | }; 132 | 133 | const moveRaw = { 134 | x: e.clientX - this.pointerDownPos.x, 135 | y: e.clientY - this.pointerDownPos.y, 136 | }; 137 | this.movement = { 138 | x: this.config.axis === 'y' ? 0 : moveRaw.x, 139 | y: this.config.axis === 'x' ? 0 : moveRaw.y, 140 | }; 141 | 142 | this.offset = { 143 | x: this.start.x + this.movement.x, 144 | y: this.start.y + this.movement.y, 145 | }; 146 | 147 | this.prev = { x: e.clientX, y: e.clientY }; 148 | 149 | this.emitChange({ 150 | down: true, 151 | movement: { ...this.movement }, 152 | offset: { ...this.offset }, 153 | velocity: { ...this.velocity }, 154 | event: e, 155 | cancel: this.cancel.bind(this), 156 | }); 157 | } 158 | 159 | private onUp(e: PointerEvent) { 160 | if (this.activePointerId !== e.pointerId || !this.activeEl) return; 161 | this.activeEl.releasePointerCapture(e.pointerId); 162 | 163 | this.emitEnd({ 164 | down: false, 165 | movement: { ...this.movement }, 166 | offset: { ...this.offset }, 167 | velocity: { ...this.velocity }, 168 | event: e, 169 | cancel: this.cancel.bind(this), 170 | }); 171 | 172 | this.activePointerId = null; 173 | this.pointerCaptured = false; 174 | } 175 | 176 | cancel() { 177 | if (this.activeEl && this.activePointerId !== null) { 178 | this.activeEl.releasePointerCapture(this.activePointerId); 179 | this.activePointerId = null; 180 | this.activeEl = null; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/hooks/observers/useScrollProgress.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef } from 'react'; 2 | import { MotionValue } from '@raidipesh78/re-motion'; 3 | 4 | import { useValue, withSpring } from '../../animation'; 5 | import { ScrollGesture } from '../../gestures/controllers/ScrollGesture'; 6 | import { useRecognizer } from '../../gestures/hooks/useRecognizer'; 7 | import { type Descriptor } from '../../animation/types'; 8 | 9 | type SupportedEdgeUnit = 'px' | 'vw' | 'vh' | '%'; 10 | type EdgeUnit = `${number}${SupportedEdgeUnit}`; 11 | type NamedEdges = 'start' | 'end' | 'center'; 12 | type EdgeString = NamedEdges | EdgeUnit | `${number}`; 13 | type Edge = EdgeString | number; 14 | type ProgressIntersection = [number, number]; 15 | type Intersection = `${Edge} ${Edge}`; 16 | type ScrollOffset = Array; 17 | 18 | export interface UseScrollProgressOptions { 19 | target?: RefObject; 20 | axis?: 'x' | 'y'; 21 | offset?: ScrollOffset; 22 | animate?: boolean; 23 | toDescriptor?: (t: number) => Descriptor; 24 | } 25 | 26 | export function useScrollProgress( 27 | refs: Window | RefObject, 28 | { 29 | target, 30 | axis = 'y', 31 | offset = ['start start', 'end end'], 32 | animate = true, 33 | toDescriptor = (v: number) => withSpring(v), 34 | }: UseScrollProgressOptions = {} 35 | ): { 36 | scrollYProgress: MotionValue; 37 | scrollXProgress: MotionValue; 38 | } { 39 | const [yProgress, setYProgress] = useValue(0); 40 | const [xProgress, setXProgress] = useValue(0); 41 | const rangeRef = useRef<[number, number]>([0, 0]); 42 | 43 | useEffect(() => { 44 | const containerEl = 45 | refs instanceof Window ? window : (refs.current as HTMLElement); 46 | const targetEl = target?.current ?? document.documentElement; 47 | 48 | rangeRef.current = getScrollRange( 49 | offset as [Intersection, Intersection], 50 | targetEl, 51 | containerEl, 52 | axis 53 | ); 54 | }, [refs, target, axis, offset]); 55 | 56 | useRecognizer(ScrollGesture, refs, (e) => { 57 | const pos = axis === 'y' ? e.offset.y : e.offset.x; 58 | const [start, end] = rangeRef.current; 59 | 60 | const raw = 61 | end === start ? (pos < start ? 0 : 1) : (pos - start) / (end - start); 62 | 63 | const t = Math.min(Math.max(raw, 0), 1); 64 | const apply = animate ? toDescriptor : (v: number) => v; 65 | 66 | if (axis === 'y') { 67 | setYProgress(apply(t)); 68 | setXProgress(0); 69 | } else { 70 | setXProgress(apply(t)); 71 | setYProgress(0); 72 | } 73 | }); 74 | 75 | return { scrollYProgress: yProgress, scrollXProgress: xProgress }; 76 | } 77 | 78 | function getScroll(el: HTMLElement | Window, axis: 'x' | 'y') { 79 | if (el instanceof HTMLElement) { 80 | return axis === 'y' ? el.scrollTop : el.scrollLeft; 81 | } 82 | return axis === 'y' ? window.scrollY : window.scrollX; 83 | } 84 | 85 | function getSize(el: HTMLElement | Window, axis: 'x' | 'y') { 86 | if (el instanceof HTMLElement) { 87 | return axis === 'y' ? el.clientHeight : el.clientWidth; 88 | } 89 | return axis === 'y' ? window.innerHeight : window.innerWidth; 90 | } 91 | 92 | function getScrollRange( 93 | [startMarker, endMarker]: [Intersection, Intersection], 94 | targetEl: HTMLElement, 95 | containerEl: HTMLElement | Window, 96 | axis: 'x' | 'y' 97 | ): [number, number] { 98 | return [ 99 | resolveMarker(startMarker, targetEl, containerEl, axis), 100 | resolveMarker(endMarker, targetEl, containerEl, axis), 101 | ]; 102 | } 103 | 104 | function resolveMarker( 105 | marker: Intersection, 106 | targetEl: HTMLElement, 107 | containerEl: HTMLElement | Window, 108 | axis: 'x' | 'y' 109 | ): number { 110 | const [tMark, cMark = tMark] = marker.trim().split(/\s+/) as [ 111 | EdgeString, 112 | EdgeString 113 | ]; 114 | 115 | if (containerEl instanceof HTMLElement) { 116 | const tRect = targetEl.getBoundingClientRect(); 117 | const cRect = containerEl.getBoundingClientRect(); 118 | const scroll = getScroll(containerEl, axis); 119 | const elementStart = 120 | (axis === 'y' ? tRect.top - cRect.top : tRect.left - cRect.left) + scroll; 121 | const elementSize = axis === 'y' ? tRect.height : tRect.width; 122 | const containerSize = getSize(containerEl, axis); 123 | 124 | const elemPos = resolveEdge( 125 | tMark, 126 | elementStart, 127 | elementSize, 128 | containerSize 129 | ); 130 | const contPos = resolveEdge(cMark, 0, containerSize, containerSize); 131 | return elemPos - contPos; 132 | } else { 133 | const elemPos = parseEdgeValue(tMark, axis, targetEl, false); 134 | const contPos = parseEdgeValue(cMark, axis, window, true); 135 | return elemPos - contPos; 136 | } 137 | } 138 | 139 | function resolveEdge( 140 | edge: EdgeString, 141 | base: number, 142 | size: number, 143 | containerSize: number 144 | ): number { 145 | if (edge === 'start') return base; 146 | if (edge === 'center') return base + size / 2; 147 | if (edge === 'end') return base + size; 148 | 149 | const m = edge.match(/^(-?\d+(?:\.\d+)?)(px|%|vw|vh)?$/); 150 | if (!m) throw new Error(`Invalid edge marker “${edge}”`); 151 | 152 | const n = parseFloat(m[1]); 153 | const unit = m[2] as SupportedEdgeUnit | undefined; 154 | 155 | switch (unit) { 156 | case 'px': 157 | return base + n; 158 | case '%': 159 | return base + (n / 100) * size; 160 | case 'vw': 161 | return base + (n / 100) * containerSize; 162 | case 'vh': 163 | return base + (n / 100) * containerSize; 164 | default: 165 | return base + n * size; 166 | } 167 | } 168 | 169 | function parseEdgeValue( 170 | edge: EdgeString, 171 | axis: 'x' | 'y', 172 | el: HTMLElement | Window, 173 | isContainer: boolean 174 | ): number { 175 | const scrollTarget = isContainer ? el : (el as HTMLElement); 176 | const base = isContainer 177 | ? 0 178 | : (() => { 179 | if (!(el instanceof HTMLElement)) 180 | throw new Error('Expected HTMLElement for element-relative edge'); 181 | const rect = el.getBoundingClientRect(); 182 | const pageScroll = 183 | axis === 'y' 184 | ? window.pageYOffset || window.scrollY 185 | : window.pageXOffset || window.scrollX; 186 | return (axis === 'y' ? rect.top : rect.left) + pageScroll; 187 | })(); 188 | 189 | const size = isContainer 190 | ? getSize(el, axis) 191 | : (() => { 192 | if (!(el instanceof HTMLElement)) throw new Error(); 193 | const rect = el.getBoundingClientRect(); 194 | return axis === 'y' ? rect.height : rect.width; 195 | })(); 196 | 197 | return resolveEdge(edge, base, size, getSize(scrollTarget, axis)); 198 | } 199 | -------------------------------------------------------------------------------- /src/animation/hooks/useValue.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react'; 2 | import { delay, sequence, loop, MotionValue } from '@raidipesh78/re-motion'; 3 | 4 | import { buildAnimation, buildParallel } from '../drivers'; 5 | import { filterCallbackOptions, isDescriptor } from '../helpers'; 6 | import type { Primitive, Descriptor, Controls } from '../types'; 7 | 8 | type Widen = T extends number ? number : T extends string ? string : T; 9 | 10 | type ValueReturn = T extends Primitive 11 | ? MotionValue> 12 | : T extends Primitive[] 13 | ? MotionValue>[] 14 | : { [K in keyof T]: MotionValue> }; 15 | 16 | type Base = Primitive | Primitive[] | Record; 17 | 18 | export function useValue( 19 | initial: T 20 | ): [ValueReturn, (to: Base | Descriptor) => void, Controls] { 21 | const controllerRef = useRef(null); 22 | 23 | const value = useMemo(() => { 24 | if (Array.isArray(initial)) { 25 | return initial.map((v) => new MotionValue(v)); 26 | } 27 | 28 | if (typeof initial === 'object') { 29 | return Object.fromEntries( 30 | Object.entries(initial).map(([k, v]) => [k, new MotionValue(v)]) 31 | ); 32 | } 33 | 34 | return new MotionValue(initial); 35 | }, []) as ValueReturn; 36 | 37 | function set(to: Base | Descriptor) { 38 | let ctrl: Controls | null = null; 39 | 40 | if (Array.isArray(initial)) { 41 | ctrl = handleArray( 42 | value as Array>, 43 | to as Primitive[] | Descriptor 44 | ); 45 | } else if (typeof initial === 'object') { 46 | ctrl = handleObject( 47 | value as Record>, 48 | to as Record | Descriptor 49 | ); 50 | } else { 51 | ctrl = handlePrimitive( 52 | value as MotionValue, 53 | to as Primitive | Descriptor 54 | ); 55 | } 56 | 57 | controllerRef.current = ctrl; 58 | if (ctrl) ctrl.start(); 59 | } 60 | 61 | const controls = { 62 | start: () => controllerRef.current?.start(), 63 | pause: () => controllerRef.current?.pause(), 64 | resume: () => controllerRef.current?.resume(), 65 | cancel: () => controllerRef.current?.cancel(), 66 | reset: () => controllerRef.current?.reset(), 67 | }; 68 | 69 | return [value, set, controls] as const; 70 | } 71 | 72 | function handlePrimitive( 73 | mv: MotionValue, 74 | to: Primitive | Descriptor 75 | ) { 76 | if (typeof to === 'number' || typeof to === 'string') { 77 | mv.set(to); 78 | return null; 79 | } 80 | 81 | if (to.type === 'sequence') { 82 | const animations = to.options?.animations ?? []; 83 | const ctrls = animations.map((step) => buildAnimation(mv, step)); 84 | return sequence(ctrls, to.options); 85 | } 86 | 87 | if (to.type === 'loop') { 88 | const animation = to.options?.animation; 89 | if (!animation) return null; 90 | 91 | if (animation.type === 'sequence') { 92 | const animations = animation.options?.animations ?? []; 93 | const ctrls = animations.map((step) => buildAnimation(mv, step)); 94 | return loop(sequence(ctrls), to.options?.iterations ?? 0, to.options); 95 | } 96 | 97 | return loop( 98 | buildAnimation(mv, animation), 99 | to.options?.iterations ?? 0, 100 | to.options 101 | ); 102 | } 103 | 104 | return buildAnimation(mv, to); 105 | } 106 | 107 | function handleArray( 108 | mvs: Array>, 109 | to: Primitive[] | Descriptor 110 | ) { 111 | if (!isDescriptor(to)) { 112 | (to as Primitive[]).forEach((val, i) => { 113 | mvs[i]?.set(val); 114 | }); 115 | return null; 116 | } 117 | 118 | const desc = to as Descriptor; 119 | 120 | const mvsRecord = Object.fromEntries( 121 | mvs.map((mv, idx) => [idx.toString(), mv]) 122 | ) as Record>; 123 | 124 | switch (desc.type) { 125 | case 'sequence': { 126 | const ctrls = desc.options!.animations!.map((step) => 127 | step.type === 'delay' 128 | ? delay(step.options?.delay ?? 0) 129 | : buildParallel(mvsRecord, { 130 | ...step, 131 | to: Array.isArray(step.to) 132 | ? Object.fromEntries( 133 | (step.to as Primitive[]).map((v, i) => [i.toString(), v]) 134 | ) 135 | : step.to, 136 | }) 137 | ); 138 | 139 | return sequence(ctrls, desc.options); 140 | } 141 | 142 | case 'loop': { 143 | const inner = desc.options!.animation!; 144 | 145 | if (inner.type === 'sequence') { 146 | const seqCtrls = inner.options!.animations!.map((step) => 147 | buildParallel(mvsRecord, { 148 | ...step, 149 | to: Array.isArray(step.to) 150 | ? Object.fromEntries( 151 | (step.to as Primitive[]).map((v, i) => [i.toString(), v]) 152 | ) 153 | : step.to, 154 | }) 155 | ); 156 | 157 | const seq = sequence( 158 | seqCtrls, 159 | filterCallbackOptions(inner.options, true) 160 | ); 161 | 162 | return loop( 163 | seq, 164 | desc.options!.iterations ?? 0, 165 | filterCallbackOptions(desc.options, true) 166 | ); 167 | } 168 | 169 | const par = buildParallel(mvsRecord, inner); 170 | return loop( 171 | par, 172 | desc.options!.iterations ?? 0, 173 | filterCallbackOptions(desc.options, true) 174 | ); 175 | } 176 | 177 | case 'decay': 178 | return buildParallel(mvsRecord, desc); 179 | 180 | default: 181 | return buildParallel(mvsRecord, desc); 182 | } 183 | } 184 | 185 | function handleObject( 186 | mvs: Record>, 187 | to: Record | Descriptor 188 | ) { 189 | if (isDescriptor(to)) { 190 | switch (to.type) { 191 | case 'sequence': { 192 | const ctrls = to.options!.animations!.map((step) => 193 | step.type === 'delay' 194 | ? delay(step.options!.delay ?? 0) 195 | : buildParallel(mvs, step) 196 | ); 197 | return sequence(ctrls, to.options); 198 | } 199 | 200 | case 'loop': { 201 | const inner = to.options!.animation!; 202 | if (inner.type === 'sequence') { 203 | const ctrls = inner.options!.animations!.map((s) => 204 | buildParallel(mvs, s) 205 | ); 206 | return loop( 207 | sequence(ctrls, filterCallbackOptions(inner.options, true)), 208 | to.options!.iterations ?? 0, 209 | filterCallbackOptions(to.options, true) 210 | ); 211 | } 212 | return loop( 213 | buildParallel(mvs, inner), 214 | to.options!.iterations ?? 0, 215 | filterCallbackOptions(to.options, true) 216 | ); 217 | } 218 | 219 | case 'decay': 220 | return buildParallel(mvs, to); 221 | 222 | default: 223 | return buildParallel(mvs, to); 224 | } 225 | } 226 | 227 | Object.entries(to).forEach(([k, v]) => { 228 | mvs[k]?.set(v); 229 | }); 230 | 231 | return null; 232 | } 233 | --------------------------------------------------------------------------------