├── docs ├── public │ ├── robots.txt │ ├── pug.jpg │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── server │ └── tsconfig.json ├── app.config.ts ├── components │ ├── ui │ │ ├── separator │ │ │ ├── index.ts │ │ │ └── Separator.vue │ │ ├── scroll-area │ │ │ ├── index.ts │ │ │ ├── ScrollArea.vue │ │ │ └── ScrollBar.vue │ │ ├── collapsible │ │ │ ├── index.ts │ │ │ ├── CollapsibleTrigger.vue │ │ │ ├── CollapsibleContent.vue │ │ │ └── Collapsible.vue │ │ ├── tabs │ │ │ ├── index.ts │ │ │ ├── Tabs.vue │ │ │ ├── TabsList.vue │ │ │ ├── TabsContent.vue │ │ │ └── TabsTrigger.vue │ │ ├── tooltip │ │ │ ├── index.ts │ │ │ ├── TooltipTrigger.vue │ │ │ ├── TooltipProvider.vue │ │ │ └── Tooltip.vue │ │ ├── accordion │ │ │ ├── index.ts │ │ │ ├── Accordion.vue │ │ │ ├── AccordionItem.vue │ │ │ └── AccordionContent.vue │ │ ├── dialog │ │ │ ├── DialogClose.vue │ │ │ ├── DialogTrigger.vue │ │ │ ├── DialogHeader.vue │ │ │ ├── Dialog.vue │ │ │ ├── DialogFooter.vue │ │ │ ├── index.ts │ │ │ ├── DialogDescription.vue │ │ │ └── DialogTitle.vue │ │ ├── sheet │ │ │ ├── SheetClose.vue │ │ │ ├── SheetTrigger.vue │ │ │ ├── SheetHeader.vue │ │ │ ├── Sheet.vue │ │ │ ├── SheetFooter.vue │ │ │ ├── SheetTitle.vue │ │ │ └── SheetDescription.vue │ │ ├── navigation-menu │ │ │ ├── NavigationMenuItem.vue │ │ │ ├── NavigationMenuLink.vue │ │ │ ├── NavigationMenuList.vue │ │ │ ├── index.ts │ │ │ ├── NavigationMenuIndicator.vue │ │ │ ├── NavigationMenu.vue │ │ │ └── NavigationMenuTrigger.vue │ │ ├── breadcrumb │ │ │ ├── Breadcrumb.vue │ │ │ ├── BreadcrumbItem.vue │ │ │ ├── BreadcrumbList.vue │ │ │ ├── BreadcrumbPage.vue │ │ │ ├── index.ts │ │ │ ├── BreadcrumbSeparator.vue │ │ │ ├── BreadcrumbLink.vue │ │ │ └── BreadcrumbEllipsis.vue │ │ ├── card │ │ │ ├── CardContent.vue │ │ │ ├── CardFooter.vue │ │ │ ├── CardHeader.vue │ │ │ ├── index.ts │ │ │ ├── CardDescription.vue │ │ │ ├── CardTitle.vue │ │ │ └── Card.vue │ │ ├── alert │ │ │ ├── AlertDescription.vue │ │ │ ├── AlertTitle.vue │ │ │ ├── Alert.vue │ │ │ └── index.ts │ │ ├── drawer │ │ │ ├── DrawerFooter.vue │ │ │ ├── DrawerHeader.vue │ │ │ ├── index.ts │ │ │ ├── Drawer.vue │ │ │ ├── DrawerOverlay.vue │ │ │ ├── DrawerTitle.vue │ │ │ ├── DrawerDescription.vue │ │ │ └── DrawerContent.vue │ │ ├── command │ │ │ ├── CommandShortcut.vue │ │ │ ├── index.ts │ │ │ ├── CommandEmpty.vue │ │ │ ├── CommandSeparator.vue │ │ │ ├── Command.vue │ │ │ ├── CommandList.vue │ │ │ ├── CommandItem.vue │ │ │ ├── CommandDialog.vue │ │ │ ├── CommandGroup.vue │ │ │ └── CommandInput.vue │ │ └── badge │ │ │ ├── Badge.vue │ │ │ └── index.ts │ ├── content │ │ ├── ProseEm.vue │ │ ├── ProseHr.vue │ │ ├── ProseTbody.vue │ │ ├── LoadingData.vue │ │ ├── ProseLi.vue │ │ ├── ProseStrong.vue │ │ ├── ProseP.vue │ │ ├── FieldGroup.vue │ │ ├── ProseThead.vue │ │ ├── ProseTr.vue │ │ ├── CardGroup.vue │ │ ├── TeamCardGroup.vue │ │ ├── ProseBlockquote.vue │ │ ├── ProseTd.vue │ │ ├── ProseTh.vue │ │ ├── ProseUl.vue │ │ ├── ProseOl.vue │ │ ├── ProseTable.vue │ │ ├── Callout.vue │ │ ├── Stack.vue │ │ ├── ProseCodeInline.vue │ │ ├── ProseA.vue │ │ ├── Accordion.vue │ │ ├── CodeGroup.vue │ │ ├── Shortcut.vue │ │ ├── Kbd.vue │ │ ├── Steps.vue │ │ ├── Stackblitz.vue │ │ ├── ProseH1.vue │ │ ├── ProseH4.vue │ │ ├── ProseH5.vue │ │ ├── ProseH6.vue │ │ ├── PmX.vue │ │ ├── ProseH3.vue │ │ ├── PmRun.vue │ │ ├── ProseH2.vue │ │ ├── ReadMore.vue │ │ ├── AccordionItem.vue │ │ ├── PmInstall.vue │ │ ├── SmartIcon.vue │ │ ├── ButtonLink.vue │ │ ├── ProseImg.vue │ │ ├── ProsePre.vue │ │ └── Tabs.vue │ ├── layout │ │ ├── DocsFooter.vue │ │ ├── header │ │ │ ├── NavMobile.vue │ │ │ └── Logo.vue │ │ ├── PrevNext.vue │ │ ├── EditLink.vue │ │ ├── AsideTree.vue │ │ ├── AsideTreeItemButton.vue │ │ ├── Aside.vue │ │ └── Breadcrumb.vue │ └── demo │ │ ├── drag │ │ └── index.vue │ │ ├── hover │ │ └── index.vue │ │ ├── press │ │ └── index.vue │ │ ├── basic │ │ └── index.vue │ │ ├── use-spring │ │ └── index.vue │ │ ├── in-view │ │ └── index.vue │ │ ├── reorder-layout │ │ ├── array-utils.ts │ │ ├── AddIcon.vue │ │ └── ingredients.ts │ │ ├── drag-with-constraints │ │ └── index.vue │ │ ├── while-drag │ │ └── index.vue │ │ ├── keyframes │ │ └── index.vue │ │ ├── gestures │ │ └── index.vue │ │ ├── scroll-animate │ │ └── index.vue │ │ ├── use-animate │ │ └── index.vue │ │ ├── exit │ │ └── index.vue │ │ ├── html-content │ │ └── index.vue │ │ ├── shared-layout │ │ └── ingredients.ts │ │ ├── custom │ │ └── index.vue │ │ ├── reorder │ │ └── index.vue │ │ ├── variants │ │ └── index.vue │ │ ├── unwrap-element │ │ └── index.vue │ │ ├── layout-group │ │ └── ToggleContent.vue │ │ └── multiple │ │ └── index.vue ├── content │ ├── index.md │ ├── 5.hooks │ │ ├── _dir.yaml │ │ ├── 3.use-animate-frame.md │ │ └── 1.use-animate.md │ ├── 3.animation │ │ ├── _dir.yaml │ │ └── layout.md │ ├── 2.components │ │ ├── _dir.yaml │ │ └── 5.reorder.md │ ├── 1.getting-started │ │ ├── _dir.yml │ │ ├── 2.installation.md │ │ └── 1.introduction.md │ └── 4.motion-value │ │ ├── _dir.yaml │ │ ├── 2.use-motion-template.md │ │ ├── 7. use-velocity.md │ │ └── 6.use-transform.md ├── tsconfig.json ├── composables │ ├── useCollapsedMap.ts │ ├── useEditLink.ts │ └── useBreadcrumb.ts ├── lib │ ├── utils.ts │ └── motion.ts ├── .gitignore ├── components.json ├── pages │ └── index.vue └── README.md ├── playground ├── nuxt │ ├── public │ │ ├── robots.txt │ │ ├── pug.jpg │ │ └── favicon.ico │ ├── server │ │ └── tsconfig.json │ ├── pages │ │ ├── animate-variants │ │ │ └── Test.vue │ │ ├── child2.vue │ │ ├── use-test.ts │ │ ├── layout-id-tabs │ │ │ ├── array-utils.ts │ │ │ └── ingredients.ts │ │ ├── reorder-layout │ │ │ ├── array-utils.ts │ │ │ ├── AddIcon.vue │ │ │ └── ingredients.ts │ │ ├── motion-config │ │ │ └── index.vue │ │ ├── child.vue │ │ ├── number-counter │ │ │ └── AdditionIcon.vue │ │ ├── app-card │ │ │ └── card.ts │ │ ├── drag-to-reorder-lists │ │ │ └── index.vue │ │ ├── press.vue │ │ ├── animate-present-initial.vue │ │ ├── in-view.vue │ │ └── layout.vue │ ├── tsconfig.json │ ├── .gitignore │ ├── tailwind.config.js │ ├── components │ │ ├── sandbox │ │ │ └── content.vue │ │ └── sandbox.vue │ ├── assets │ │ └── css │ │ │ └── tailwind.css │ ├── package.json │ ├── nuxt.config.ts │ ├── MyTransition.vue │ └── demos │ │ ├── Animation.vue │ │ └── use-follow-pointer.ts └── vite │ ├── playwright.config.ts │ ├── src │ ├── views │ │ ├── Home.vue │ │ ├── dynamic-variant.vue │ │ └── [...404].vue │ ├── env.d.ts │ └── main.ts │ ├── postcss.config.js │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ └── tsconfig.json ├── packages ├── motion │ ├── src │ │ ├── components │ │ │ ├── animate-presence │ │ │ │ ├── utils.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── use-presence.ts │ │ │ ├── reorder │ │ │ │ ├── types.ts │ │ │ │ ├── index.ts │ │ │ │ └── context.ts │ │ │ ├── motion-config │ │ │ │ ├── index.ts │ │ │ │ ├── context.ts │ │ │ │ └── types.ts │ │ │ ├── motion │ │ │ │ ├── m.ts │ │ │ │ ├── renderSlotFragments.ts │ │ │ │ └── index.ts │ │ │ ├── use-force-update.ts │ │ │ ├── index.ts │ │ │ ├── lazy-motion │ │ │ │ └── context.ts │ │ │ ├── LayoutGroup.vue │ │ │ ├── use-slot-change-index.ts │ │ │ ├── RowValue.vue │ │ │ ├── context.ts │ │ │ ├── __tests__ │ │ │ │ └── motion-config.test.tsx │ │ │ └── group.ts │ │ ├── state │ │ │ ├── index.ts │ │ │ ├── utils │ │ │ │ ├── is-variant-labels.ts │ │ │ │ └── is-present.ts │ │ │ ├── event.ts │ │ │ └── create-visual-element.ts │ │ ├── events │ │ │ ├── utils │ │ │ │ ├── index.ts │ │ │ │ └── is-primary-pointer.ts │ │ │ ├── types.ts │ │ │ ├── index.ts │ │ │ ├── add-dom-event.ts │ │ │ ├── add-pointer-event.ts │ │ │ └── event-info.ts │ │ ├── utils │ │ │ ├── is.ts │ │ │ ├── delay.ts │ │ │ ├── get-context-window.ts │ │ │ ├── index.ts │ │ │ ├── use-page-in-view.ts │ │ │ ├── use-dom-ref.ts │ │ │ └── use-animation-frame.ts │ │ ├── types │ │ │ ├── transform.ts │ │ │ ├── index.ts │ │ │ ├── common.ts │ │ │ └── framer-motion.ts │ │ ├── shared │ │ │ └── test.ts │ │ ├── vite-env.d.ts │ │ ├── features │ │ │ ├── gestures │ │ │ │ ├── index.ts │ │ │ │ ├── drag │ │ │ │ │ ├── utils │ │ │ │ │ │ └── is.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── pan │ │ │ │ │ └── types.ts │ │ │ │ ├── focus │ │ │ │ │ └── types.ts │ │ │ │ ├── hover │ │ │ │ │ └── types.ts │ │ │ │ ├── types.ts │ │ │ │ ├── press │ │ │ │ │ └── types.ts │ │ │ │ └── in-view │ │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── feature.ts │ │ │ ├── layout │ │ │ │ ├── utils.ts │ │ │ │ ├── config.ts │ │ │ │ └── types.ts │ │ │ ├── animation │ │ │ │ ├── types.ts │ │ │ │ └── calc-child-stagger.ts │ │ │ ├── dom-animation.ts │ │ │ └── dom-max.ts │ │ ├── animation │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ └── hooks │ │ │ │ └── use-reduced-motion.ts │ │ ├── projection │ │ │ ├── geometry │ │ │ │ ├── delta-calc.ts │ │ │ │ ├── delta-apply.ts │ │ │ │ └── models.ts │ │ │ ├── utils │ │ │ │ ├── each-axis.ts │ │ │ │ └── measure.ts │ │ │ └── node │ │ │ │ └── types.ts │ │ ├── value │ │ │ ├── use-will-change │ │ │ │ ├── types.ts │ │ │ │ ├── is.ts │ │ │ │ └── add-will-change.ts │ │ │ ├── use-time.ts │ │ │ ├── index.ts │ │ │ ├── use-motion-value-event.ts │ │ │ ├── use-computed.ts │ │ │ ├── __tests__ │ │ │ │ └── use-transform.test.tsx │ │ │ └── use-velocity.ts │ │ ├── global.d.ts │ │ └── index.ts │ └── tsconfig.json └── plugins │ ├── package.json │ ├── src │ └── resolver │ │ └── index.ts │ ├── build.config.ts │ └── tsconfig.json ├── commitlint.config.js ├── pnpm-workspace.yaml ├── .github ├── FUNDING.yml ├── workflows │ └── release.yaml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tsconfig.json ├── playwright.config.ts ├── tests └── animate-presence.spec.ts ├── .gitignore └── LICENSE /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/nuxt/public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/vite/playwright.config.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/vite/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/motion/src/components/animate-presence/utils.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/motion/src/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './motion-state' 2 | -------------------------------------------------------------------------------- /packages/motion/src/events/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './is-primary-pointer' 2 | -------------------------------------------------------------------------------- /docs/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/motion/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export const isSSR = typeof window === 'undefined' 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | docs: { 3 | 4 | }, 5 | }) 6 | -------------------------------------------------------------------------------- /docs/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Separator } from './Separator.vue' 2 | -------------------------------------------------------------------------------- /playground/nuxt/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /docs/public/pug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/docs/public/pug.jpg -------------------------------------------------------------------------------- /packages/motion/src/types/transform.ts: -------------------------------------------------------------------------------- 1 | export type { TransformOptions } from 'framer-motion/dom' 2 | -------------------------------------------------------------------------------- /docs/components/content/ProseEm.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Motion Vue 3 | navigation: false 4 | --- 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/components/content/ProseHr.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/motion/src/shared/test.ts: -------------------------------------------------------------------------------- 1 | export const delay = (ms: number) => new Promise(r => setTimeout(r, ms)) 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'playground/*' 4 | - 'docs' 5 | - 'new-docs' 6 | -------------------------------------------------------------------------------- /docs/components/content/ProseTbody.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /playground/nuxt/public/pug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/playground/nuxt/public/pug.jpg -------------------------------------------------------------------------------- /docs/components/content/LoadingData.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/content/5.hooks/_dir.yaml: -------------------------------------------------------------------------------- 1 | title: Hooks 2 | navigation.icon: 'lucide:hammer' 3 | navigation.redirect: /hooks/use-animate 4 | -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /packages/motion/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/content/3.animation/_dir.yaml: -------------------------------------------------------------------------------- 1 | title: Animation 2 | navigation.icon: 'lucide:move' 3 | navigation.redirect: /animation/layout 4 | -------------------------------------------------------------------------------- /playground/nuxt/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/playground/nuxt/public/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [rick-hup] 4 | custom: ["https://motion.dev/plus"] 5 | -------------------------------------------------------------------------------- /docs/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/docs/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiondivision/motion-vue/HEAD/docs/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /playground/vite/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /docs/components/content/ProseLi.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/components/content/ProseStrong.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/content/2.components/_dir.yaml: -------------------------------------------------------------------------------- 1 | title: Components 2 | navigation.icon: 'lucide:component' 3 | navigation.redirect: /components/motion 4 | -------------------------------------------------------------------------------- /docs/components/content/ProseP.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/motion/src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | export function delay(fn: () => void) { 2 | return Promise.resolve().then(() => { 3 | fn() 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /docs/components/content/FieldGroup.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/components/content/ProseThead.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Getting started 2 | navigation.icon: 'lucide:rocket' 3 | navigation.redirect: /getting-started/introduction 4 | -------------------------------------------------------------------------------- /docs/content/4.motion-value/_dir.yaml: -------------------------------------------------------------------------------- 1 | title: Motion Value 2 | navigation.icon: 'lucide:grab' 3 | navigation.redirect: /motion-value/use-motion-value-event 4 | -------------------------------------------------------------------------------- /docs/components/content/ProseTr.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/components/layout/DocsFooter.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/components/ui/scroll-area/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ScrollArea } from './ScrollArea.vue' 2 | export { default as ScrollBar } from './ScrollBar.vue' 3 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hover' 2 | export * from './press' 3 | export * from './in-view' 4 | export * from './drag' 5 | -------------------------------------------------------------------------------- /docs/composables/useCollapsedMap.ts: -------------------------------------------------------------------------------- 1 | export function useCollapsedMap() { 2 | return useState>('docs-collapsed-map', () => new Map()) 3 | } 4 | -------------------------------------------------------------------------------- /packages/motion/src/events/types.ts: -------------------------------------------------------------------------------- 1 | import type { Point } from 'framer-motion' 2 | 3 | /** @public */ 4 | export interface EventInfo { 5 | point: Point 6 | } 7 | -------------------------------------------------------------------------------- /packages/motion/src/components/reorder/types.ts: -------------------------------------------------------------------------------- 1 | import type { Axis } from 'framer-motion' 2 | 3 | export interface ItemData { 4 | value: T 5 | layout: Axis 6 | } 7 | -------------------------------------------------------------------------------- /docs/components/content/CardGroup.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/motion/src/animation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks/use-animate' 2 | export * from './hooks/use-animation-controls' 3 | export * from './hooks/use-reduced-motion' 4 | -------------------------------------------------------------------------------- /docs/components/content/TeamCardGroup.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/motion/src/components/animate-presence/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AnimatePresence } from './AnimatePresence.vue' 2 | export type { AnimatePresenceProps } from './types' 3 | -------------------------------------------------------------------------------- /docs/components/content/ProseBlockquote.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/motion/src/components/motion-config/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MotionConfig } from './MotionConfig.vue' 2 | export { provideMotionConfig, useMotionConfig } from './context' 3 | -------------------------------------------------------------------------------- /packages/motion/src/projection/geometry/delta-calc.ts: -------------------------------------------------------------------------------- 1 | import type { Axis } from 'framer-motion' 2 | 3 | export function calcLength(axis: Axis) { 4 | return axis.max - axis.min 5 | } 6 | -------------------------------------------------------------------------------- /packages/motion/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transform' 2 | export * from './framer-motion' 3 | export * from './motion-values' 4 | export * from './state' 5 | export * from './common' 6 | -------------------------------------------------------------------------------- /docs/components/content/ProseTd.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/motion/src/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './utils' 3 | export * from './add-pointer-event' 4 | export * from './event-info' 5 | export * from './add-dom-event' 6 | -------------------------------------------------------------------------------- /packages/motion/src/projection/utils/each-axis.ts: -------------------------------------------------------------------------------- 1 | type Callback = (axis: 'x' | 'y') => void 2 | 3 | export function eachAxis(callback: Callback) { 4 | return [callback('x'), callback('y')] 5 | } 6 | -------------------------------------------------------------------------------- /packages/motion/src/value/use-will-change/types.ts: -------------------------------------------------------------------------------- 1 | import type { MotionValue } from 'framer-motion' 2 | 3 | export interface WillChange extends MotionValue { 4 | add: (name: string) => void 5 | } 6 | -------------------------------------------------------------------------------- /playground/nuxt/pages/animate-variants/Test.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /docs/components/content/ProseTh.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/components/content/ProseUl.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/drag/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isHTMLElement(value: any): value is HTMLElement { 2 | return typeof value === 'object' && value !== null && 'nodeType' in value 3 | } 4 | -------------------------------------------------------------------------------- /docs/components/content/ProseOl.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /playground/nuxt/pages/child2.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | child2 8 | 9 | 10 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /docs/components/content/ProseTable.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /playground/vite/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /docs/content/4.motion-value/2.use-motion-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useMotionTemplate 3 | description: 4 | navigation.icon: 'lucide:rotate-3d' 5 | --- 6 | 7 | - [Motion React-useMotionTemplate](https://motion.dev/docs/react-use-motion-template) 8 | -------------------------------------------------------------------------------- /packages/motion/src/projection/geometry/delta-apply.ts: -------------------------------------------------------------------------------- 1 | import type { Axis } from 'framer-motion' 2 | 3 | export function translateAxis(axis: Axis, distance: number) { 4 | axis.min = axis.min + distance 5 | axis.max = axis.max + distance 6 | } 7 | -------------------------------------------------------------------------------- /packages/motion/src/utils/get-context-window.ts: -------------------------------------------------------------------------------- 1 | import type { VisualElement } from 'framer-motion' 2 | 3 | export function getContextWindow({ current }: VisualElement) { 4 | return current ? current.ownerDocument.defaultView : null 5 | } 6 | -------------------------------------------------------------------------------- /playground/vite/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { router } from './router' 3 | import App from './App.vue' 4 | 5 | import './style.css' 6 | 7 | const app = createApp(App) 8 | app.use(router) 9 | app.mount('#app') 10 | -------------------------------------------------------------------------------- /docs/components/ui/collapsible/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Collapsible } from './Collapsible.vue' 2 | export { default as CollapsibleContent } from './CollapsibleContent.vue' 3 | export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue' 4 | -------------------------------------------------------------------------------- /packages/motion/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createContext' 2 | export * from './use-in-view' 3 | export * from './use-animation-frame' 4 | export * from './get-context-window' 5 | export * from './use-dom-ref' 6 | export * from './use-page-in-view' 7 | -------------------------------------------------------------------------------- /docs/components/demo/drag/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /docs/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tabs } from './Tabs.vue' 2 | export { default as TabsContent } from './TabsContent.vue' 3 | export { default as TabsList } from './TabsList.vue' 4 | export { default as TabsTrigger } from './TabsTrigger.vue' 5 | -------------------------------------------------------------------------------- /packages/motion/src/components/reorder/index.ts: -------------------------------------------------------------------------------- 1 | import Group from './Group.vue' 2 | import Item from './Item.vue' 3 | 4 | export const ReorderGroup = Group 5 | export const ReorderItem = Item 6 | export const Reorder = { 7 | Group, 8 | Item, 9 | } 10 | -------------------------------------------------------------------------------- /packages/motion/src/state/utils/is-variant-labels.ts: -------------------------------------------------------------------------------- 1 | import type { VariantLabels } from 'motion-dom' 2 | 3 | export function isVariantLabels(value: any): value is VariantLabels { 4 | return typeof value === 'string' || value === false || Array.isArray(value) 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "baseUrl": ".", 6 | "types": ["node"] 7 | }, 8 | "include": ["./packages/**/*"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /playground/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleResolution": "Node" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /packages/motion/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { visualElementStore } from 'framer-motion' 2 | 3 | type VisualElementStore = typeof visualElementStore 4 | // @ts-ignore 5 | declare module 'framer-motion/dist/es/render/store.mjs' { 6 | export const visualElementStore: VisualElementStore 7 | } 8 | -------------------------------------------------------------------------------- /packages/motion/src/value/use-time.ts: -------------------------------------------------------------------------------- 1 | import { useAnimationFrame } from '@/utils/use-animation-frame' 2 | import { motionValue } from 'framer-motion/dom' 3 | 4 | export function useTime() { 5 | const time = motionValue(0) 6 | useAnimationFrame(t => time.set(t)) 7 | return time 8 | } 9 | -------------------------------------------------------------------------------- /playground/vite/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /docs/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './Tooltip.vue' 2 | export { default as TooltipContent } from './TooltipContent.vue' 3 | export { default as TooltipProvider } from './TooltipProvider.vue' 4 | export { default as TooltipTrigger } from './TooltipTrigger.vue' 5 | -------------------------------------------------------------------------------- /packages/motion/src/projection/geometry/models.ts: -------------------------------------------------------------------------------- 1 | import type { Axis, Box } from 'framer-motion' 2 | 3 | export const createAxis = (): Axis => ({ min: 0, max: 0 }) 4 | 5 | export function createBox(): Box { 6 | return { 7 | x: createAxis(), 8 | y: createAxis(), 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/components/ui/accordion/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Accordion } from './Accordion.vue' 2 | export { default as AccordionContent } from './AccordionContent.vue' 3 | export { default as AccordionItem } from './AccordionItem.vue' 4 | export { default as AccordionTrigger } from './AccordionTrigger.vue' 5 | -------------------------------------------------------------------------------- /packages/motion/src/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from './feature' 2 | export * from './gestures' 3 | export * from './layout/layout' 4 | export * from './gestures/pan' 5 | export * from './feature-manager' 6 | export * from './animation/animation' 7 | export * from './dom-max' 8 | export * from './dom-animation' 9 | -------------------------------------------------------------------------------- /packages/motion/src/state/utils/is-present.ts: -------------------------------------------------------------------------------- 1 | import { doneCallbacks } from '@/components/animate-presence/presence' 2 | import type { VisualElement } from 'framer-motion' 3 | 4 | export function isPresent(visualElement: VisualElement) { 5 | return !doneCallbacks.has(visualElement.current as Element) 6 | } 7 | -------------------------------------------------------------------------------- /packages/motion/src/components/motion/m.ts: -------------------------------------------------------------------------------- 1 | import type { MotionComponent } from '@/components/motion/types' 2 | import { createMotionComponentWithFeatures } from '@/components/motion/utils' 3 | 4 | export const m = createMotionComponentWithFeatures() 5 | export const M = m.create('div') as unknown as MotionComponent 6 | -------------------------------------------------------------------------------- /docs/components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/components/ui/sheet/SheetClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/motion/src/components/use-force-update.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { ref } from 'vue' 3 | 4 | export function useForceUpdate(): [() => void, Ref] { 5 | const key = ref(0) 6 | function forceUpdate() { 7 | key.value++ 8 | } 9 | 10 | return [forceUpdate, key] 11 | } 12 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /packages/motion/src/animation/utils.ts: -------------------------------------------------------------------------------- 1 | import type { AnimationControls } from './types' 2 | 3 | export function isAnimationControls(v?: unknown): v is AnimationControls { 4 | return ( 5 | v !== null 6 | && typeof v === 'object' 7 | && typeof (v as AnimationControls).start === 'function' 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /docs/components/ui/sheet/SheetTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/components/content/Callout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /playground/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /docs/components/ui/tooltip/TooltipTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/motion/src/value/use-will-change/is.ts: -------------------------------------------------------------------------------- 1 | import type { WillChange } from '@/value/use-will-change/types' 2 | import { isMotionValue } from 'framer-motion/dom' 3 | 4 | export function isWillChangeMotionValue(value: any): value is WillChange { 5 | return Boolean(isMotionValue(value) && ((value as unknown) as WillChange).add) 6 | } 7 | -------------------------------------------------------------------------------- /docs/components/layout/header/NavMobile.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/tooltip/TooltipProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/motion/src/state/event.ts: -------------------------------------------------------------------------------- 1 | import type { VariantType } from '@/types' 2 | 3 | export type MotionEventNames = 4 | | 'motionstart' 5 | | 'motioncomplete' 6 | 7 | export function motionEvent(name: MotionEventNames, target: VariantType, isExit?: boolean) { 8 | return new CustomEvent(name, { detail: { target, isExit } }) 9 | } 10 | -------------------------------------------------------------------------------- /docs/components/content/Stack.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/motion/src/events/add-dom-event.ts: -------------------------------------------------------------------------------- 1 | export function addDomEvent( 2 | target: EventTarget, 3 | eventName: string, 4 | handler: EventListener, 5 | options: AddEventListenerOptions = { passive: true }, 6 | ) { 7 | target.addEventListener(eventName, handler, options) 8 | 9 | return () => target.removeEventListener(eventName, handler) 10 | } 11 | -------------------------------------------------------------------------------- /playground/vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import jsx from '@vitejs/plugin-vue-jsx' 4 | 5 | export default defineConfig({ 6 | plugins: [vue(), jsx()], 7 | server: { 8 | port: 5173, 9 | }, 10 | resolve: { 11 | alias: { 12 | '@': '/src', 13 | }, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /docs/components/ui/collapsible/CollapsibleTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/components/ui/navigation-menu/NavigationMenuItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/lib/motion.ts: -------------------------------------------------------------------------------- 1 | import type { MotionProps } from 'motion-v' 2 | 3 | export const slideUp: Partial> = { 4 | variants: { 5 | hidden: { opacity: 0, y: 30, rotate: 3 }, 6 | visible: { opacity: 1, y: 0, rotate: 0 }, 7 | }, 8 | transition: { 9 | type: 'spring', 10 | stiffness: 260, 11 | damping: 50, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/motion/src/components/animate-presence/types.ts: -------------------------------------------------------------------------------- 1 | export interface AnimatePresenceProps { 2 | // 动画模式: wait(等待前一个完成), popLayout(弹出布局), sync(同步) 3 | mode?: 'wait' 4 | | 'popLayout' 5 | | 'sync' 6 | // 是否显示初始动画 7 | initial?: boolean 8 | as?: string 9 | custom?: any 10 | onExitComplete?: VoidFunction 11 | anchorX?: 'left' | 'right' 12 | } 13 | -------------------------------------------------------------------------------- /packages/motion/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Motion, motion, type MotionProps } from './motion' 2 | export * from './animate-presence' 3 | export * from './motion-config' 4 | export * from './reorder' 5 | export { default as RowValue } from './RowValue.vue' 6 | export * from './lazy-motion' 7 | export * from './motion/m' 8 | export { mountedStates } from '@/state' 9 | -------------------------------------------------------------------------------- /packages/motion/src/value/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-computed' 2 | export * from './use-combine-values' 3 | export * from './use-transform' 4 | export * from './use-time' 5 | export * from './use-motion-template' 6 | export * from './use-motion-value-event' 7 | export * from './use-spring' 8 | export * from './use-scroll' 9 | export * from './use-velocity' 10 | -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/components/ui/card/CardContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/card/CardHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card.vue' 2 | export { default as CardContent } from './CardContent.vue' 3 | export { default as CardDescription } from './CardDescription.vue' 4 | export { default as CardFooter } from './CardFooter.vue' 5 | export { default as CardHeader } from './CardHeader.vue' 6 | export { default as CardTitle } from './CardTitle.vue' 7 | -------------------------------------------------------------------------------- /docs/components/demo/hover/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/card/CardDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/alert/AlertDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/alert/AlertTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/drawer/DrawerFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/drawer/DrawerHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/2.installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: How to install motion-v in your app. 4 | navigation.icon: 'lucide:play' 5 | --- 6 | 7 | ### Installation 8 | 9 | :pm-install{name="motion-v"} 10 | 11 | ### Nuxt Module 12 | 13 | ```ts 14 | export default defineNuxtConfig({ 15 | modules: [ 16 | // ... 17 | 'motion-v/nuxt', 18 | ], 19 | }) 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/motion/src/components/motion/renderSlotFragments.ts: -------------------------------------------------------------------------------- 1 | import { Fragment, type VNode } from 'vue' 2 | 3 | export function renderSlotFragments(children?: VNode[]): VNode[] { 4 | if (!children) 5 | return [] 6 | return children.flatMap((child) => { 7 | if (child.type === Fragment) 8 | return renderSlotFragments(child.children as VNode[]) 9 | 10 | return [child] 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /packages/motion/src/events/add-pointer-event.ts: -------------------------------------------------------------------------------- 1 | import { type EventListenerWithPointInfo, addDomEvent, addPointerInfo } from '@/events' 2 | 3 | export function addPointerEvent( 4 | target: EventTarget, 5 | eventName: string, 6 | handler: EventListenerWithPointInfo, 7 | options?: AddEventListenerOptions, 8 | ) { 9 | return addDomEvent(target, eventName, addPointerInfo(handler), options) 10 | } 11 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/pan/types.ts: -------------------------------------------------------------------------------- 1 | import type { PanInfo } from '@/features/gestures/pan/PanSession' 2 | 3 | export interface PanProps { 4 | onPanSessionStart?: (event: PointerEvent, info: PanInfo) => void 5 | onPanStart?: (event: PointerEvent, info: PanInfo) => void 6 | onPan?: (event: PointerEvent, info: PanInfo) => void 7 | onPanEnd?: (event: PointerEvent, info: PanInfo) => void 8 | } 9 | -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb/BreadcrumbItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/motion/src/components/lazy-motion/context.ts: -------------------------------------------------------------------------------- 1 | import type { Feature } from '@/features' 2 | import { createContext } from '@/utils' 3 | import type { Ref } from 'vue' 4 | 5 | export type LazyMotionContext = { 6 | features: Ref> 7 | strict: boolean 8 | } 9 | export const [useLazyMotionContext, lazyMotionContextProvider] = createContext('LazyMotionContext') 10 | -------------------------------------------------------------------------------- /packages/motion/src/components/motion/index.ts: -------------------------------------------------------------------------------- 1 | export { type MotionProps } from './types' 2 | import type { MotionComponent } from '@/components/motion/types' 3 | import { createMotionComponentWithFeatures } from './utils' 4 | import { domMax } from '@/features/dom-max' 5 | 6 | export const motion = createMotionComponentWithFeatures(domMax) 7 | export const Motion = motion.create('div') as unknown as MotionComponent 8 | -------------------------------------------------------------------------------- /docs/components/ui/command/CommandShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /playground/nuxt/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './components/**/*.{js,vue,ts}', 5 | './demos/**/*.{js,vue,ts}', 6 | './layouts/**/*.vue', 7 | './pages/**/*.vue', 8 | './plugins/**/*.{js,ts}', 9 | './app.vue', 10 | './error.vue', 11 | ], 12 | theme: { 13 | extend: {}, 14 | }, 15 | plugins: [], 16 | } 17 | -------------------------------------------------------------------------------- /docs/components/content/ProseCodeInline.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /docs/components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/components/demo/press/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /docs/components/ui/card/CardTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/components/ui/sheet/SheetHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/components/demo/basic/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 17 | 18 | -------------------------------------------------------------------------------- /packages/motion/src/features/feature.ts: -------------------------------------------------------------------------------- 1 | import type { MotionState } from '@/state/motion-state' 2 | 3 | export class Feature { 4 | state: MotionState 5 | 6 | constructor(state: MotionState) { 7 | this.state = state 8 | } 9 | 10 | beforeMount(): void {} 11 | 12 | mount(): void {} 13 | 14 | unmount(): void {} 15 | 16 | update?(): void {} 17 | 18 | beforeUpdate?(): void {} 19 | 20 | beforeUnmount?(): void {} 21 | } 22 | -------------------------------------------------------------------------------- /playground/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Motion Vue Playground 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@motion-vue/playground-vite", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "motion-plus-vue": "0.1.0", 13 | "motion-v": "workspace:*", 14 | "vue": "^3.4.0", 15 | "vue-router": "^4.5.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/motion/src/components/LayoutGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /playground/nuxt/pages/use-test.ts: -------------------------------------------------------------------------------- 1 | import { type Slot, computed, useSlots } from 'vue' 2 | 3 | export function useSlotChangeIndex() { 4 | let prevSlots: ReturnType> | undefined 5 | let index = 0 6 | const slots = useSlots() 7 | return computed(() => { 8 | const newSlots = slots.default?.() 9 | if (prevSlots !== newSlots) { 10 | index++ 11 | prevSlots = newSlots 12 | } 13 | return index 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /docs/components/ui/sheet/Sheet.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb/BreadcrumbList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/ui/tabs/Tabs.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/components/ui/tooltip/Tooltip.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/motion/src/animation/hooks/use-reduced-motion.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import type { Ref } from 'vue' 3 | import { useMediaQuery } from '@vueuse/core' 4 | 5 | /** 6 | * Reactive prefers-reduced-motion. 7 | */ 8 | export function useReducedMotion(options: { window?: Window } = {}): Ref { 9 | const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)', options) 10 | 11 | return computed(() => reducedMotion.value) 12 | } 13 | -------------------------------------------------------------------------------- /docs/components/content/ProseA.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/components/ui/badge/Badge.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/components/ui/card/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/ui/sheet/SheetFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/motion/src/components/use-slot-change-index.ts: -------------------------------------------------------------------------------- 1 | import { type Slot, computed, useSlots } from 'vue' 2 | 3 | export function useSlotChangeIndex() { 4 | let prevSlots: ReturnType> | undefined 5 | let index = 0 6 | const slots = useSlots() 7 | return computed(() => { 8 | const newSlots = slots.default?.() 9 | if (prevSlots !== newSlots) { 10 | index++ 11 | prevSlots = newSlots 12 | } 13 | return index 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /docs/components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/components/content/Accordion.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/components/demo/use-spring/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb/BreadcrumbPage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "new-york", 4 | "typescript": true, 5 | "tsConfigPath": ".nuxt/tsconfig.json", 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "assets/css/tailwind.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "framework": "nuxt", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils" 17 | } 18 | } -------------------------------------------------------------------------------- /packages/motion/src/projection/node/types.ts: -------------------------------------------------------------------------------- 1 | import type { Box, Delta, ResolvedValues } from 'framer-motion' 2 | 3 | export interface Measurements { 4 | animationId: number 5 | measuredBox: Box 6 | layoutBox: Box 7 | latestValues: ResolvedValues 8 | source: number 9 | } 10 | export interface LayoutUpdateData { 11 | layout: Box 12 | snapshot: Measurements 13 | delta: Delta 14 | layoutDelta: Delta 15 | hasLayoutChanged: boolean 16 | hasRelativeTargetChanged: boolean 17 | } 18 | -------------------------------------------------------------------------------- /packages/motion/src/components/reorder/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from '@/utils' 2 | import type { Box } from 'framer-motion' 3 | import type { Ref } from 'vue' 4 | 5 | export interface ReorderContextProps { 6 | axis?: Ref<'x' | 'y'> 7 | registerItem?: (item: T, layout: Box) => void 8 | updateOrder?: (item: T, offset: number, velocity: number) => void 9 | } 10 | 11 | export const [useReorderContext, reorderContextProvider] = createContext>('ReorderContext') 12 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/focus/types.ts: -------------------------------------------------------------------------------- 1 | import type { VariantType } from '@/types' 2 | import type { VariantLabels } from 'motion-dom' 3 | 4 | export type FocusProps = { 5 | /** 6 | * @deprecated Use `whileFocus` instead. 7 | */ 8 | focus?: VariantLabels | VariantType 9 | /** 10 | * Variant to apply when the element is focused. 11 | */ 12 | whileFocus?: VariantLabels | VariantType 13 | onFocus?: (e: FocusEvent) => void 14 | onBlur?: (e: FocusEvent) => void 15 | } 16 | -------------------------------------------------------------------------------- /packages/motion/src/state/create-visual-element.ts: -------------------------------------------------------------------------------- 1 | import { isSVGElement } from '@/state/utils' 2 | import type { AsTag } from '@/types' 3 | import { HTMLVisualElement } from 'framer-motion/dist/es/render/html/HTMLVisualElement.mjs' 4 | import { SVGVisualElement } from 'framer-motion/dist/es/render/svg/SVGVisualElement.mjs' 5 | 6 | export function createVisualElement(Component: AsTag, options: any) { 7 | return isSVGElement(Component as any) ? new SVGVisualElement(options) : new HTMLVisualElement(options) 8 | } 9 | -------------------------------------------------------------------------------- /docs/components/demo/in-view/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 20 | 21 | -------------------------------------------------------------------------------- /docs/components/ui/collapsible/CollapsibleContent.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/components/demo/reorder-layout/array-utils.ts: -------------------------------------------------------------------------------- 1 | export function removeItem([...arr]: T[], item: T) { 2 | const index = arr.indexOf(item) 3 | index > -1 && arr.splice(index, 1) 4 | return arr 5 | } 6 | 7 | export function closestItem(arr: T[], item: T) { 8 | const index = arr.indexOf(item) 9 | if (index === -1) { 10 | return arr[0] 11 | } 12 | else if (index === arr.length - 1) { 13 | return arr[arr.length - 2] 14 | } 15 | else { 16 | return arr[index + 1] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/components/ui/alert/Alert.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Breadcrumb } from './Breadcrumb.vue' 2 | export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue' 3 | export { default as BreadcrumbItem } from './BreadcrumbItem.vue' 4 | export { default as BreadcrumbLink } from './BreadcrumbLink.vue' 5 | export { default as BreadcrumbList } from './BreadcrumbList.vue' 6 | export { default as BreadcrumbPage } from './BreadcrumbPage.vue' 7 | export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue' 8 | -------------------------------------------------------------------------------- /playground/nuxt/pages/layout-id-tabs/array-utils.ts: -------------------------------------------------------------------------------- 1 | export function removeItem([...arr]: T[], item: T) { 2 | const index = arr.indexOf(item) 3 | index > -1 && arr.splice(index, 1) 4 | return arr 5 | } 6 | 7 | export function closestItem(arr: T[], item: T) { 8 | const index = arr.indexOf(item) 9 | if (index === -1) { 10 | return arr[0] 11 | } 12 | else if (index === arr.length - 1) { 13 | return arr[arr.length - 2] 14 | } 15 | else { 16 | return arr[index + 1] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /playground/nuxt/pages/reorder-layout/array-utils.ts: -------------------------------------------------------------------------------- 1 | export function removeItem([...arr]: T[], item: T) { 2 | const index = arr.indexOf(item) 3 | index > -1 && arr.splice(index, 1) 4 | return arr 5 | } 6 | 7 | export function closestItem(arr: T[], item: T) { 8 | const index = arr.indexOf(item) 9 | if (index === -1) { 10 | return arr[0] 11 | } 12 | else if (index === arr.length - 1) { 13 | return arr[arr.length - 2] 14 | } 15 | else { 16 | return arr[index + 1] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/components/ui/accordion/Accordion.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugins", 3 | "version": "1.7.4", 4 | "private": true, 5 | "description": "", 6 | "author": "", 7 | "license": "ISC", 8 | "keywords": [], 9 | "scripts": { 10 | "build:plugins": "unbuild" 11 | }, 12 | "dependencies": { 13 | "@nuxt/kit": "^3.14.1592", 14 | "@nuxt/schema": "^3.14.1592", 15 | "motion-v": "workspace:*" 16 | }, 17 | "devDependencies": { 18 | "unbuild": "^2.0.0", 19 | "unplugin-vue-components": "^0.28.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/components/layout/PrevNext.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 11 | 15 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/content/CodeGroup.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/components/demo/drag-with-constraints/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/components/demo/while-drag/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 16 | 17 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /packages/motion/src/features/layout/utils.ts: -------------------------------------------------------------------------------- 1 | import type { IProjectionNode, VisualElement } from 'framer-motion' 2 | 3 | export function getClosestProjectingNode( 4 | visualElement?: VisualElement< 5 | unknown, 6 | unknown, 7 | { allowProjection?: boolean } 8 | >, 9 | ): IProjectionNode | undefined { 10 | if (!visualElement) 11 | return undefined 12 | 13 | return visualElement.options.allowProjection !== false 14 | ? visualElement.projection 15 | : getClosestProjectingNode(visualElement.parent) 16 | } 17 | -------------------------------------------------------------------------------- /docs/components/ui/drawer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Drawer } from './Drawer.vue' 2 | export { default as DrawerContent } from './DrawerContent.vue' 3 | export { default as DrawerDescription } from './DrawerDescription.vue' 4 | export { default as DrawerFooter } from './DrawerFooter.vue' 5 | export { default as DrawerHeader } from './DrawerHeader.vue' 6 | export { default as DrawerOverlay } from './DrawerOverlay.vue' 7 | export { default as DrawerTitle } from './DrawerTitle.vue' 8 | export { DrawerClose, DrawerPortal, DrawerTrigger } from 'vaul-vue' 9 | -------------------------------------------------------------------------------- /docs/components/content/Shortcut.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | {{ computedValue }} 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb/BreadcrumbSeparator.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /playground/nuxt/pages/motion-config/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 13 | Hello 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/components/demo/reorder-layout/AddIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/components/ui/navigation-menu/NavigationMenuLink.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/motion/src/components/animate-presence/use-presence.ts: -------------------------------------------------------------------------------- 1 | import { onMounted } from 'vue' 2 | import { useMotionElm } from '@/components/hooks/use-motion-elm' 3 | 4 | export const presenceMeasure = new Map() 5 | export function usePresence() { 6 | const motionElement = useMotionElm() 7 | onMounted(() => { 8 | presenceMeasure.set(motionElement.value, true) 9 | }) 10 | 11 | const safeToRemove = () => { 12 | if (motionElement.value) { 13 | presenceMeasure.delete(motionElement.value) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/nuxt/pages/reorder-layout/AddIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/components/demo/keyframes/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/ui/drawer/Drawer.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/components/content/Kbd.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/components/demo/gestures/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 22 | 23 | -------------------------------------------------------------------------------- /docs/components/layout/EditLink.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 10 | 15 | 16 | 17 | 18 | {{ text }} 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb/BreadcrumbLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/components/ui/collapsible/Collapsible.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /playground/nuxt/components/sandbox/content.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /playground/nuxt/pages/child.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 19 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /packages/plugins/src/resolver/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentResolver } from 'unplugin-vue-components' 2 | 3 | const components = new Set([ 4 | 'Motion', 5 | 'AnimatePresence', 6 | 'LayoutGroup', 7 | 'MotionConfig', 8 | 'ReorderGroup', 9 | 'ReorderItem', 10 | 'M', 11 | ]) 12 | 13 | export default function (): ComponentResolver { 14 | return { 15 | type: 'component', 16 | resolve: (name: string) => { 17 | if (components.has(name)) { 18 | return { 19 | name, 20 | from: 'motion-v', 21 | } 22 | } 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/nuxt/pages/number-counter/AdditionIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 15 | 20 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/components/content/Steps.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/components/layout/AsideTree.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/motion/src/types/common.ts: -------------------------------------------------------------------------------- 1 | import type { Component, DefineComponent, ExtractPropTypes, ExtractPublicPropTypes, IntrinsicElementAttributes, MaybeRef } from 'vue' 2 | 3 | export type ComponentProps = T extends DefineComponent< 4 | ExtractPropTypes, 5 | any, 6 | any 7 | > 8 | ? ExtractPublicPropTypes 9 | : never 10 | 11 | export type ElementType = keyof IntrinsicElementAttributes 12 | export type AsTag = keyof IntrinsicElementAttributes | ({} & string) | Component // any other string 13 | 14 | export type ToRefs = { 15 | [K in keyof T]: MaybeRef 16 | } 17 | -------------------------------------------------------------------------------- /docs/components/ui/command/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Command } from './Command.vue' 2 | export { default as CommandDialog } from './CommandDialog.vue' 3 | export { default as CommandEmpty } from './CommandEmpty.vue' 4 | export { default as CommandGroup } from './CommandGroup.vue' 5 | export { default as CommandInput } from './CommandInput.vue' 6 | export { default as CommandItem } from './CommandItem.vue' 7 | export { default as CommandList } from './CommandList.vue' 8 | export { default as CommandSeparator } from './CommandSeparator.vue' 9 | export { default as CommandShortcut } from './CommandShortcut.vue' 10 | -------------------------------------------------------------------------------- /docs/pages/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/motion/src/components/RowValue.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | {{ value.get() }} 22 | 23 | -------------------------------------------------------------------------------- /packages/motion/src/value/use-will-change/add-will-change.ts: -------------------------------------------------------------------------------- 1 | import { isWillChangeMotionValue } from '@/value/use-will-change/is' 2 | import type { VisualElement } from 'framer-motion' 3 | 4 | export function addValueToWillChange( 5 | visualElement: VisualElement, 6 | key: string, 7 | ) { 8 | const willChange = visualElement.getValue('willChange') 9 | 10 | /** 11 | * It could be that a user has set willChange to a regular MotionValue, 12 | * in which case we can't add the value to it. 13 | */ 14 | if (isWillChangeMotionValue(willChange)) { 15 | return willChange.add(key) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/hover/types.ts: -------------------------------------------------------------------------------- 1 | import type { VariantType } from '@/types' 2 | import type { EventInfo } from 'framer-motion' 3 | import type { VariantLabels } from 'motion-dom' 4 | 5 | export type HoverEvent = (event: MouseEvent, info: EventInfo) => void 6 | 7 | export interface HoverProps { 8 | /** 9 | * @deprecated Use `whileHover` instead. 10 | */ 11 | hover?: VariantLabels | VariantType 12 | /** 13 | * Variant to apply when the element is hovered. 14 | */ 15 | whileHover?: VariantLabels | VariantType 16 | onHoverStart?: HoverEvent 17 | onHoverEnd?: HoverEvent 18 | } 19 | -------------------------------------------------------------------------------- /docs/components/demo/scroll-animate/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /docs/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogContent } from './DialogContent.vue' 4 | export { default as DialogDescription } from './DialogDescription.vue' 5 | export { default as DialogFooter } from './DialogFooter.vue' 6 | export { default as DialogHeader } from './DialogHeader.vue' 7 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 8 | export { default as DialogTitle } from './DialogTitle.vue' 9 | export { default as DialogTrigger } from './DialogTrigger.vue' 10 | -------------------------------------------------------------------------------- /packages/motion/src/utils/use-page-in-view.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted, ref } from 'vue' 2 | 3 | export function usePageInView() { 4 | const isInView = ref(true) 5 | 6 | const handleVisibilityChange = () => { 7 | isInView.value = !document.hidden 8 | } 9 | 10 | onMounted(() => { 11 | if (document.hidden) { 12 | handleVisibilityChange() 13 | } 14 | document.addEventListener('visibilitychange', handleVisibilityChange) 15 | }) 16 | 17 | onUnmounted(() => { 18 | document.removeEventListener('visibilitychange', handleVisibilityChange) 19 | }) 20 | 21 | return isInView 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | name: Release 3 | 4 | permissions: 5 | contents: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 18.x 26 | 27 | - run: npx changelogithub 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb/BreadcrumbEllipsis.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | More 21 | 22 | 23 | -------------------------------------------------------------------------------- /playground/nuxt/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .primary-button { 7 | @apply px-4 py-1 ml-auto text-black bg-white font-semibold rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-400; 8 | } 9 | 10 | .secondary-button { 11 | @apply px-6 py-3 bg-green-500 text-white font-bold rounded-lg shadow-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400; 12 | } 13 | 14 | .gradient-pink-violet { 15 | @apply bg-gradient-to-tr from-[#7b2ff7] to-[#f107a3]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/motion/src/events/utils/is-primary-pointer.ts: -------------------------------------------------------------------------------- 1 | export function isPrimaryPointer(event: PointerEvent) { 2 | if (event.pointerType === 'mouse') { 3 | return typeof event.button !== 'number' || event.button <= 0 4 | } 5 | else { 6 | /** 7 | * isPrimary is true for all mice buttons, whereas every touch point 8 | * is regarded as its own input. So subsequent concurrent touch points 9 | * will be false. 10 | * 11 | * Specifically match against false here as incomplete versions of 12 | * PointerEvents in very old browser might have it set as undefined. 13 | */ 14 | return event.isPrimary !== false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/motion/src/utils/use-dom-ref.ts: -------------------------------------------------------------------------------- 1 | import { getMotionElement } from '@/components/hooks/use-motion-elm' 2 | import { ref } from 'vue' 3 | 4 | export function useDomRef() { 5 | const dom = ref(null) 6 | const domProxy = new Proxy(dom, { 7 | get(target, key) { 8 | if (typeof key === 'string' || typeof key === 'symbol') { 9 | return Reflect.get(target, key) 10 | } 11 | return undefined 12 | }, 13 | set(target, key, value) { 14 | if (key === 'value') 15 | return Reflect.set(target, key, getMotionElement(value?.$el || value)) 16 | return true 17 | }, 18 | }) 19 | return domProxy 20 | } 21 | -------------------------------------------------------------------------------- /playground/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "motion-playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "build": "nuxt build", 7 | "play": "nuxt dev --port 3001 --host", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview" 10 | }, 11 | "dependencies": { 12 | "@number-flow/vue": "^0.3.2", 13 | "motion-number-vue": "latest", 14 | "motion-plus-vue": "0.1.0", 15 | "motion-v": "workspace:*", 16 | "nuxt": "3.19.0", 17 | "reka-ui": "^2.0.0", 18 | "vue": "latest", 19 | "vue-router": "latest" 20 | }, 21 | "devDependencies": { 22 | "@nuxtjs/tailwindcss": "^6.12.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/components/ui/sheet/SheetTitle.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/components/content/Stackblitz.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /docs/components/ui/drawer/DrawerOverlay.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /playground/nuxt/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: '2024-04-03', 4 | devtools: { enabled: true }, 5 | css: ['~/assets/css/tailwind.css'], 6 | modules: [ 7 | '@nuxtjs/tailwindcss', 8 | 'motion-v/nuxt', 9 | ], 10 | app: { 11 | head: { 12 | meta: [ 13 | { 14 | name: 'viewport', 15 | content: 'width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cove', 16 | }, 17 | ], 18 | }, 19 | }, 20 | vite: { 21 | optimizeDeps: { 22 | include: ['flubber'], 23 | }, 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /docs/components/content/ProseH1.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/motion/src/value/use-motion-value-event.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 监听 MotionValue 的事件 3 | * @param value MotionValue 实例 4 | * @param event 事件名称 5 | * @param callback 回调函数 6 | */ 7 | import { onUnmounted } from 'vue' 8 | import type { MotionValue, MotionValueEventCallbacks } from 'framer-motion/dom' 9 | 10 | export function useMotionValueEvent< 11 | V, 12 | EventName extends keyof MotionValueEventCallbacks, 13 | >( 14 | value: MotionValue, 15 | event: EventName, 16 | callback: MotionValueEventCallbacks[EventName], 17 | ) { 18 | const unlisten = value.on(event, callback) 19 | 20 | onUnmounted(() => { 21 | unlisten() 22 | }) 23 | 24 | return unlisten 25 | } 26 | -------------------------------------------------------------------------------- /docs/components/layout/header/Logo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 12 | 16 | 20 | 24 | {{ title }} 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/components/content/ProseH4.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/content/ProseH5.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/content/ProseH6.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/ui/sheet/SheetDescription.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/components/content/PmX.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 31 | 32 | -------------------------------------------------------------------------------- /docs/components/content/ProseH3.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/ui/command/CommandEmpty.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/content/5.hooks/3.use-animate-frame.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useAnimationFrame 3 | description: 4 | navigation.icon: 'lucide:calendar-sync' 5 | --- 6 | 7 | `useAnimateFrame` runs a callback once every animation frame. 8 | 9 | ```vue 10 | 17 | ``` 18 | 19 | ## Usage 20 | 21 | 22 | 23 | The callback is provided two arguments: 24 | 25 | - `time`: The total time in milliseconds since the callback was first called. 26 | - `delta`: The time in milliseconds since the last animation frame. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/components/content/PmRun.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 31 | 32 | -------------------------------------------------------------------------------- /docs/components/ui/drawer/DrawerTitle.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/1.introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Motion Vue is a Animation Library for Vue. It is built on top of Motion. 4 | navigation.icon: 'lucide:info' 5 | --- 6 | 7 | ## Motivations 8 | 9 | I love [Framer Motion](https://motion.dev/). it makes animation easy. 10 | 11 | ## Features 12 | 13 | - :sparkles: Declarative animation API for intuitive control 14 | - :wave: Gesture-driven animations for interactive experiences 15 | - :gear: Motion values for dynamic animations 16 | - :package: layout animations and transitions 17 | - :door: Polished enter/exit animations 18 | - :scroll: Smooth scroll-based animations and effects 19 | - :sparkles: Layout animations and transitions 20 | -------------------------------------------------------------------------------- /packages/motion/src/events/event-info.ts: -------------------------------------------------------------------------------- 1 | import { isPrimaryPointer } from '@/events' 2 | import type { EventInfo } from './types' 3 | 4 | export type EventListenerWithPointInfo = ( 5 | e: PointerEvent, 6 | info: EventInfo 7 | ) => void 8 | 9 | export function extractEventInfo( 10 | event: PointerEvent, 11 | pointType: 'page' | 'client' = 'page', 12 | ): EventInfo { 13 | return { 14 | point: { 15 | x: event[`${pointType}X`], 16 | y: event[`${pointType}Y`], 17 | }, 18 | } 19 | } 20 | 21 | export function addPointerInfo(handler: EventListenerWithPointInfo): EventListener { 22 | return (event: PointerEvent) => 23 | isPrimaryPointer(event) && handler(event, extractEventInfo(event)) 24 | } 25 | -------------------------------------------------------------------------------- /docs/components/demo/use-animate/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 19 | 26 | Item {{ i }} 27 | 28 | 29 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /docs/components/ui/command/CommandSeparator.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/components/layout/AsideTreeItemButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 15 | 16 | 17 | {{ link.title }} 18 | 19 | 20 | 24 | 29 | {{ badge.value }} 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/motion/src/components/motion-config/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from '@/utils' 2 | import type { MotionConfigState } from './types' 3 | import { type ComputedRef, computed } from 'vue' 4 | 5 | /** 6 | * Default motion configuration 7 | */ 8 | export const defaultConfig: MotionConfigState = { 9 | reducedMotion: 'never', 10 | transition: undefined, 11 | nonce: undefined, 12 | } 13 | 14 | /** 15 | * Context for sharing motion configuration with child components 16 | */ 17 | export const [injectMotionConfig, provideMotionConfig] = createContext>('MotionConfig') 18 | 19 | export function useMotionConfig() { 20 | return injectMotionConfig(computed(() => defaultConfig)) 21 | } 22 | -------------------------------------------------------------------------------- /docs/components/content/ProseH2.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/ui/tabs/TabsList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/components/content/ReadMore.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 35 | Read more at {{ computedTitle }} 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/components/ui/accordion/AccordionItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/components/ui/drawer/DrawerDescription.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/motion/src/components/context.ts: -------------------------------------------------------------------------------- 1 | import type { MotionState } from '@/state/motion-state' 2 | import { createContext } from '@/utils' 3 | import type { IProjectionNode } from 'framer-motion' 4 | import type { Ref } from 'vue' 5 | 6 | export const [injectMotion, provideMotion] = createContext('Motion') 7 | 8 | export interface NodeGroup { 9 | add: (node: IProjectionNode) => void 10 | remove: (node: IProjectionNode) => void 11 | dirty: VoidFunction 12 | } 13 | export interface LayoutGroupState { 14 | id?: string 15 | group?: NodeGroup 16 | forceRender?: VoidFunction 17 | key?: Ref 18 | } 19 | 20 | export const [injectLayoutGroup, provideLayoutGroup] = createContext('LayoutGroup') 21 | -------------------------------------------------------------------------------- /docs/components/content/AccordionItem.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 20 | {{ title }} 21 | 22 | 23 | 27 | {{ content }} 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/components/demo/exit/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | {{ show ? 'hide' : 'show' }} 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/components/ui/tabs/TabsContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/content/5.hooks/1.use-animate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useAnimate 3 | description: 4 | navigation.icon: 'lucide:play' 5 | --- 6 | 7 | `useAnimate` provides a way of using the animate function that is scoped to the elements within the provided domRef. 8 | 9 | `useAnimate` provides a way of using the animate function that is scoped to the elements within the provided domRef. 10 | 11 | This allows you to use manual animation controls, timelines, selectors scoped to your component, and automatic cleanup. 12 | 13 | It provides a scope ref, and an animate function where every DOM selector is scoped to this ref. 14 | 15 | ## Usage 16 | 17 | 18 | 19 | ## In-view animations 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/demo/html-content/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/types.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from '@/types' 2 | import type { InertiaOptions } from 'framer-motion' 3 | 4 | export interface StateHandlers { 5 | enable: VoidFunction 6 | disable: VoidFunction 7 | } 8 | 9 | export interface Gesture { 10 | isActive: (options: Options) => void 11 | subscribe: ( 12 | element: HTMLElement, 13 | stateHandlers: StateHandlers, 14 | options: Options 15 | ) => () => void 16 | } 17 | 18 | export interface DragOptions { 19 | constraints?: { 20 | top?: number 21 | right?: number 22 | bottom?: number 23 | left?: number 24 | } 25 | dragSnapToOrigin?: boolean 26 | dragElastic?: number 27 | dragMomentum?: boolean 28 | dragTransition?: InertiaOptions 29 | } 30 | -------------------------------------------------------------------------------- /packages/motion/src/features/layout/config.ts: -------------------------------------------------------------------------------- 1 | import { correctBorderRadius } from 'framer-motion/dist/es/projection/styles/scale-border-radius.mjs' 2 | import { correctBoxShadow } from 'framer-motion/dist/es/projection/styles/scale-box-shadow.mjs' 3 | 4 | export const defaultScaleCorrector = { 5 | borderRadius: { 6 | ...correctBorderRadius, 7 | applyTo: [ 8 | 'borderTopLeftRadius', 9 | 'borderTopRightRadius', 10 | 'borderBottomLeftRadius', 11 | 'borderBottomRightRadius', 12 | ], 13 | }, 14 | borderTopLeftRadius: correctBorderRadius, 15 | borderTopRightRadius: correctBorderRadius, 16 | borderBottomLeftRadius: correctBorderRadius, 17 | borderBottomRightRadius: correctBorderRadius, 18 | boxShadow: correctBoxShadow, 19 | } 20 | -------------------------------------------------------------------------------- /playground/nuxt/pages/app-card/card.ts: -------------------------------------------------------------------------------- 1 | export const CARDS = [ 2 | { 3 | id: 1, 4 | title: 'Vikings', 5 | subtitle: 'Clash of the Norse Warriors', 6 | description: 'A game about vikings', 7 | longDescription: 8 | 'A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.', 9 | image: 10 | 'https://animations-on-the-web-git-how-i-use-3066e1-emilkowalski-s-team.vercel.app/how-i-use-framer-motion/app-store-like-cards/game.webp', 11 | logo: 'https://animations-on-the-web-git-how-i-use-3066e1-emilkowalski-s-team.vercel.app/how-i-use-framer-motion/app-store-like-cards/game-logo.webp', 12 | }, 13 | ] 14 | 15 | export type Card = (typeof CARDS)[number] 16 | -------------------------------------------------------------------------------- /docs/components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/composables/useEditLink.ts: -------------------------------------------------------------------------------- 1 | export function useEditLink() { 2 | const { page } = useContent() 3 | const { enable, pattern, text, icon, placement } = useConfig().value.main.editLink 4 | 5 | const url = computed( 6 | () => pattern.replace(/:path/g, page.value._file ?? ''), 7 | ) 8 | 9 | const enabled = computed( 10 | () => enable && page.value.editLink !== false && page.value._file && url.value !== '', 11 | ) 12 | 13 | const enabledToc = computed( 14 | () => enabled.value && placement.includes('toc'), 15 | ) 16 | const enabledDocsFooter = computed( 17 | () => enabled.value && placement.includes('docsFooter'), 18 | ) 19 | 20 | return { 21 | url, 22 | text, 23 | icon, 24 | enabledToc, 25 | enabledDocsFooter, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/components/layout/Aside.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/motion/src/features/layout/types.ts: -------------------------------------------------------------------------------- 1 | import type { Box } from 'framer-motion' 2 | 3 | export interface LayoutLifecycles { 4 | onBeforeLayoutMeasure?: (box: Box) => void 5 | 6 | onLayoutMeasure?: (box: Box, prevBox: Box) => void 7 | 8 | /** 9 | * @internal 10 | */ 11 | onLayoutAnimationStart?: () => void 12 | 13 | /** 14 | * @internal 15 | */ 16 | onLayoutAnimationComplete?: () => void 17 | } 18 | 19 | export interface LayoutOptions extends LayoutLifecycles { 20 | 'layout'?: boolean | 'position' | 'size' | 'preserve-aspect' 21 | 22 | 'layoutId'?: string 23 | 'layoutScroll'?: boolean 24 | 'layoutRoot'?: boolean 25 | 'data-framer-portal-id'?: string 26 | 'crossfade'?: boolean 27 | /** 28 | * @public 29 | */ 30 | 'layoutDependency'?: any 31 | } 32 | -------------------------------------------------------------------------------- /docs/components/demo/shared-layout/ingredients.ts: -------------------------------------------------------------------------------- 1 | export interface Ingredient { 2 | icon: string 3 | label: string 4 | } 5 | 6 | export const allIngredients = [ 7 | { icon: '🍅', label: 'Tomato' }, 8 | { icon: '🥬', label: 'Lettuce' }, 9 | { icon: '🧀', label: 'Cheese' }, 10 | { icon: '🥕', label: 'Carrot' }, 11 | { icon: '🍌', label: 'Banana' }, 12 | { icon: '🫐', label: 'Blueberries' }, 13 | { icon: '🥂', label: 'Champers?' }, 14 | ] 15 | 16 | const [tomato, lettuce, cheese] = allIngredients 17 | export const initialTabs = [tomato, lettuce, cheese] 18 | 19 | export function getNextIngredient( 20 | ingredients: Ingredient[], 21 | ): Ingredient | undefined { 22 | const existing = new Set(ingredients) 23 | return allIngredients.find(ingredient => !existing.has(ingredient)) 24 | } 25 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/press/types.ts: -------------------------------------------------------------------------------- 1 | import type { VariantType } from '@/types' 2 | import type { EventInfo } from 'framer-motion' 3 | import type { VariantLabels } from 'motion-dom' 4 | 5 | export type PressEvent = ( 6 | event: PointerEvent, 7 | info: EventInfo 8 | ) => void 9 | 10 | export interface PressProps { 11 | /** 12 | * If `true`, the press gesture will attach its start listener to window. 13 | */ 14 | globalPressTarget?: boolean 15 | /** 16 | * @deprecated Use `whilePress` instead. 17 | */ 18 | press?: VariantLabels | VariantType 19 | /** 20 | * Variant to apply when the element is pressed. 21 | */ 22 | whilePress?: VariantLabels | VariantType 23 | onPressStart?: PressEvent 24 | onPress?: PressEvent 25 | onPressCancel?: PressEvent 26 | } 27 | -------------------------------------------------------------------------------- /playground/nuxt/pages/layout-id-tabs/ingredients.ts: -------------------------------------------------------------------------------- 1 | export interface Ingredient { 2 | icon: string 3 | label: string 4 | } 5 | 6 | export const allIngredients = [ 7 | { icon: '🍅', label: 'Tomato' }, 8 | { icon: '🥬', label: 'Lettuce' }, 9 | { icon: '🧀', label: 'Cheese' }, 10 | { icon: '🥕', label: 'Carrot' }, 11 | { icon: '🍌', label: 'Banana' }, 12 | { icon: '🫐', label: 'Blueberries' }, 13 | { icon: '🥂', label: 'Champers?' }, 14 | ] 15 | 16 | const [tomato, lettuce, cheese] = allIngredients 17 | export const initialTabs = [tomato, lettuce, cheese] 18 | 19 | export function getNextIngredient( 20 | ingredients: Ingredient[], 21 | ): Ingredient | undefined { 22 | const existing = new Set(ingredients) 23 | return allIngredients.find(ingredient => !existing.has(ingredient)) 24 | } 25 | -------------------------------------------------------------------------------- /packages/motion/src/features/animation/types.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from '@/types' 2 | 3 | /** 4 | * Core animation update function that handles all animation state changes and execution 5 | * @param controlActiveState - Animation states that need to be updated 6 | * @param controlDelay - Animation delay time 7 | * @param directAnimate - Direct animation target value 8 | * @param directTransition - Direct animation transition config 9 | */ 10 | export interface AnimateUpdatesOptions { 11 | controlActiveState?: Partial> 12 | controlDelay?: number 13 | directAnimate?: Options['animate'] 14 | directTransition?: Options['transition'] 15 | isExit?: boolean 16 | } 17 | 18 | export type AnimateUpdates = (options?: AnimateUpdatesOptions) => Promise | (() => Promise) 19 | -------------------------------------------------------------------------------- /packages/plugins/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | import { dependencies } from './package.json' 3 | 4 | export default defineBuildConfig([ 5 | { 6 | name: 'Nuxt module', 7 | entries: ['./src/nuxt/index.ts'], 8 | outDir: '../motion/dist', 9 | clean: false, 10 | declaration: 'node16', 11 | rollup: { 12 | emitCJS: true, 13 | }, 14 | externals: [ 15 | ...Object.keys(dependencies), 16 | ], 17 | }, 18 | 19 | { 20 | name: 'Unplugin-vue-component Resolver', 21 | entries: ['./src/resolver/index'], 22 | outDir: '../motion/dist', 23 | clean: false, 24 | declaration: 'node16', 25 | externals: [ 26 | 'unplugin-vue-components', 27 | ], 28 | rollup: { 29 | emitCJS: true, 30 | }, 31 | }, 32 | ]) 33 | -------------------------------------------------------------------------------- /playground/nuxt/pages/drag-to-reorder-lists/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 16 | 22 | {{ item }} 23 | 24 | 25 | 26 | 27 | 29 | -------------------------------------------------------------------------------- /docs/components/ui/accordion/AccordionContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/motion/src/features/dom-animation.ts: -------------------------------------------------------------------------------- 1 | import { AnimationFeature } from '@/features/animation/animation' 2 | import { PressGesture } from '@/features/gestures/press' 3 | import { HoverGesture } from '@/features/gestures/hover' 4 | import { InViewGesture } from '@/features/gestures/in-view' 5 | import { FocusGesture } from '@/features/gestures/focus' 6 | // import { ProjectionFeature } from '@/features/layout/projection' 7 | // import { DragGesture } from '@/features/gestures/drag' 8 | // import { LayoutFeature } from '@/features/layout/layout' 9 | // import { PanGesture } from '@/features/gestures/pan' 10 | import type { Feature } from '@/features/feature' 11 | 12 | export const domAnimation: Array = [ 13 | AnimationFeature, 14 | PressGesture, 15 | HoverGesture, 16 | InViewGesture, 17 | FocusGesture, 18 | ] 19 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | export default defineConfig({ 4 | testDir: './tests', 5 | fullyParallel: true, 6 | forbidOnly: !!process.env.CI, 7 | retries: process.env.CI ? 2 : 0, 8 | workers: process.env.CI ? 1 : undefined, 9 | reporter: 'html', 10 | use: { 11 | baseURL: 'http://localhost:5173', 12 | trace: 'on-first-retry', 13 | }, 14 | 15 | projects: [ 16 | { 17 | name: 'chromium', 18 | use: { ...devices['Desktop Chrome'] }, 19 | }, 20 | { 21 | name: 'webkit', 22 | use: { ...devices['Desktop Safari'] }, 23 | }, 24 | ], 25 | 26 | webServer: { 27 | command: 'npm run dev', 28 | url: 'http://localhost:5173', 29 | reuseExistingServer: !process.env.CI, 30 | cwd: './playground/vite', 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /docs/components/demo/custom/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 25 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/components/demo/reorder-layout/ingredients.ts: -------------------------------------------------------------------------------- 1 | export interface Ingredient { 2 | icon: string 3 | label: string 4 | } 5 | 6 | export const allIngredients = [ 7 | { icon: '🍅', label: 'Tomato' }, 8 | { icon: '🥬', label: 'Lettuce' }, 9 | { icon: '🧀', label: 'Cheese' }, 10 | { icon: '🥕', label: 'Carrot' }, 11 | { icon: '🍌', label: 'Banana' }, 12 | { icon: '🫐', label: 'Blueberries' }, 13 | { icon: '🥂', label: 'Champers?' }, 14 | ] 15 | 16 | const [tomato, lettuce, cheese] = allIngredients 17 | export const initialTabs = [tomato, lettuce, cheese] 18 | 19 | export function getNextIngredient( 20 | ingredients: Ingredient[], 21 | ): Ingredient | undefined { 22 | const existing = new Set(ingredients.map(ingredient => ingredient.label)) 23 | return allIngredients.find(ingredient => !existing.has(ingredient.label)) 24 | } 25 | -------------------------------------------------------------------------------- /packages/motion/src/components/motion-config/types.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from '@/types' 2 | 3 | /** 4 | * Motion configuration state shared through context 5 | */ 6 | export interface MotionConfigState { 7 | /** Default transition settings for animations */ 8 | transition?: Options['transition'] 9 | /** 10 | * @deprecated Use `reducedMotion` instead 11 | */ 12 | reduceMotion?: 'user' | 'never' | 'always' 13 | /** Controls motion reduction based on user preference or explicit setting */ 14 | reducedMotion?: 'user' | 'never' | 'always' 15 | /** Custom nonce for CSP compliance with inline styles */ 16 | nonce?: string 17 | /** Options for the inView prop */ 18 | inViewOptions?: Options['inViewOptions'] 19 | } 20 | 21 | /** Props interface matching the config state */ 22 | export type MotionConfigProps = MotionConfigState 23 | -------------------------------------------------------------------------------- /playground/nuxt/pages/reorder-layout/ingredients.ts: -------------------------------------------------------------------------------- 1 | export interface Ingredient { 2 | icon: string 3 | label: string 4 | } 5 | 6 | export const allIngredients = [ 7 | { icon: '🍅', label: 'Tomato' }, 8 | { icon: '🥬', label: 'Lettuce' }, 9 | { icon: '🧀', label: 'Cheese' }, 10 | { icon: '🥕', label: 'Carrot' }, 11 | { icon: '🍌', label: 'Banana' }, 12 | { icon: '🫐', label: 'Blueberries' }, 13 | { icon: '🥂', label: 'Champers?' }, 14 | ] 15 | 16 | const [tomato, lettuce, cheese] = allIngredients 17 | export const initialTabs = [tomato, lettuce, cheese] 18 | 19 | export function getNextIngredient( 20 | ingredients: Ingredient[], 21 | ): Ingredient | undefined { 22 | const existing = new Set(ingredients.map(ingredient => ingredient.label)) 23 | return allIngredients.find(ingredient => !existing.has(ingredient.label)) 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **1. Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **2. IMPORTANT: Provide a CodeSandbox reproduction of the bug** 14 | 15 | **3. Steps to reproduce** 16 | 17 | Steps to reproduce the behavior: 18 | 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **4. Expected behavior** 25 | 26 | A clear and concise description of what you expected to happen. 27 | 28 | **5. Video or screenshots** 29 | 30 | If applicable, add a video or screenshots to help explain the bug. 31 | 32 | **6. Environment details** 33 | 34 | If applicable, let us know which OS, browser, browser version etc you're using. 35 | -------------------------------------------------------------------------------- /docs/components/content/PmInstall.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 35 | 36 | -------------------------------------------------------------------------------- /packages/plugins/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "moduleDetection": "force", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "resolveJsonModule": true, 9 | "allowImportingTsExtensions": true, 10 | "allowJs": true, 11 | "strict": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitOverride": true, 14 | "noImplicitThis": true, 15 | "noUncheckedIndexedAccess": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noEmit": true, 19 | "noEmitOnError": true, 20 | "outDir": "./dist", 21 | "esModuleInterop": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "isolatedModules": true, 24 | "verbatimModuleSyntax": true, 25 | "skipLibCheck": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /playground/nuxt/components/sandbox.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 15 | 16 | SANDBOX 17 | 21 | Replay 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/components/layout/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | {{ breadcrumb.title }} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/composables/useBreadcrumb.ts: -------------------------------------------------------------------------------- 1 | interface BreadcrumbItem { 2 | title: string 3 | href: string 4 | } 5 | 6 | export function useBreadcrumb(url: string): BreadcrumbItem[] { 7 | const { navigation } = useContent() 8 | 9 | const breadcrumbItems: BreadcrumbItem[] = [] 10 | // Remove empty segments 11 | const segments = url.split('/').filter(segment => segment !== '') 12 | 13 | // Construct breadcrumb for each segment 14 | let href = '' 15 | let nav = navigation.value 16 | 17 | if (!nav) 18 | return [] 19 | 20 | for (let i = 0; i < segments.length; i++) { 21 | const segment = segments[i].replace('.html', '') 22 | href += `/${segment}` 23 | const page = nav?.find((x: any) => (x._path as string) === href) 24 | nav = page?.children 25 | breadcrumbItems.push({ title: page?.title ?? segment, href }) 26 | } 27 | return breadcrumbItems 28 | } 29 | -------------------------------------------------------------------------------- /docs/components/demo/reorder/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 19 | 26 | {{ item }} 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /packages/motion/src/utils/use-animation-frame.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeUpdate, onUnmounted } from 'vue' 2 | import { cancelFrame, frame } from 'framer-motion/dom' 3 | import type { FrameData } from 'framer-motion/dom' 4 | 5 | export type FrameCallback = (timestamp: number, delta: number) => void 6 | 7 | export function useAnimationFrame(callback: FrameCallback) { 8 | let initialTimestamp = 0 9 | 10 | const provideTimeSinceStart = ({ timestamp, delta }: FrameData) => { 11 | if (!initialTimestamp) 12 | initialTimestamp = timestamp 13 | 14 | callback(timestamp - initialTimestamp, delta) 15 | } 16 | 17 | const cancel = () => cancelFrame(provideTimeSinceStart) 18 | 19 | onBeforeUpdate(() => { 20 | cancel() 21 | frame.update(provideTimeSinceStart, true) 22 | }) 23 | 24 | onUnmounted(() => cancel()) 25 | 26 | frame.update(provideTimeSinceStart, true) 27 | } 28 | -------------------------------------------------------------------------------- /tests/animate-presence.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test.describe('AnimatePresence', () => { 4 | test('AnimatePresence test', async ({ page }) => { 5 | await page.goto('/animate-presence') 6 | 7 | const toggle = await page.waitForSelector('#toggle') 8 | 9 | await toggle.click() 10 | 11 | const animatePresenceItem = await page.waitForSelector('#animate-presence-item') 12 | 13 | await expect(animatePresenceItem).toBeDefined() 14 | 15 | await toggle.click() 16 | await page.waitForTimeout(100) 17 | const opacity = await animatePresenceItem?.evaluate(el => window.getComputedStyle(el).opacity) 18 | expect(+opacity).toBeLessThan(1) 19 | expect(await animatePresenceItem.isVisible()).toBe(true) 20 | await page.waitForTimeout(400) 21 | expect(await animatePresenceItem.isVisible()).toBe(false) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /docs/components/ui/navigation-menu/NavigationMenuList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/content/3.animation/layout.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Layout 3 | navigation.icon: 'lucide:layout-grid' 4 | description: 5 | --- 6 | 7 | like [Motion React](https://motion.dev/docs/react-layout-animations), you can use layout prop to enable layout animations. 8 | 9 | ## Animate Flex 10 | 11 | 12 | 13 | ## Shared layout 14 | 15 | 16 | 17 | ::stackblitz 18 | --- 19 | src: https://stackblitz.com/edit/vitejs-vite-e19yd3ez?ctl=1&embed=1&file=src%2FApp.vue&hideExplorer=1 20 | title: motion-use-spring 21 | --- 22 | :: 23 | 24 | ## Resources 25 | 26 | - [Motion React layout animations](https://motion.dev/docs/react-layout-animations) 27 | - [flip-your-animations](https://aerotwist.com/blog/flip-your-animations/) 28 | - [framer-motion-layout-animations](https://blog.maximeheckel.com/posts/framer-motion-layout-animations) 29 | -------------------------------------------------------------------------------- /docs/components/content/SmartIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 21 | 22 | {{ name }} 26 | 27 | 33 | 34 | -------------------------------------------------------------------------------- /playground/nuxt/MyTransition.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /playground/nuxt/pages/press.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 21 | Press Me! 22 | 23 | 24 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /playground/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "preserve", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "useDefineForClassFields": true, 7 | 8 | /* Paths */ 9 | "baseUrl": ".", 10 | "module": "ESNext", 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "paths": { 15 | "@/*": ["./src/*"] 16 | }, 17 | "resolveJsonModule": true, 18 | "allowImportingTsExtensions": true, 19 | /* Linting */ 20 | "strict": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noEmit": true, 23 | "skipLibCheck": true, 24 | "isolatedModules": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true 27 | }, 28 | "references": [{ "path": "./tsconfig.node.json" }], 29 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 30 | } 31 | -------------------------------------------------------------------------------- /packages/motion/src/features/dom-max.ts: -------------------------------------------------------------------------------- 1 | import { AnimationFeature } from '@/features/animation/animation' 2 | import { PressGesture } from '@/features/gestures/press' 3 | import { HoverGesture } from '@/features/gestures/hover' 4 | import { InViewGesture } from '@/features/gestures/in-view' 5 | import { FocusGesture } from '@/features/gestures/focus' 6 | import { ProjectionFeature } from '@/features/layout/projection' 7 | import { DragGesture } from '@/features/gestures/drag' 8 | import { LayoutFeature } from '@/features/layout/layout' 9 | import { PanGesture } from '@/features/gestures/pan' 10 | import type { Feature } from '@/features/feature' 11 | 12 | export const domMax: Array = [ 13 | AnimationFeature, 14 | PressGesture, 15 | HoverGesture, 16 | InViewGesture, 17 | FocusGesture, 18 | ProjectionFeature, 19 | PanGesture, 20 | DragGesture, 21 | LayoutFeature, 22 | ] 23 | -------------------------------------------------------------------------------- /packages/motion/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'framer-motion/dom' 2 | export { delay as delayInMs } from 'framer-motion/dist/es/utils/delay.mjs' 3 | export { addScaleCorrector } from 'framer-motion/dist/es/projection/styles/scale-correction.mjs' 4 | export { motionValue as useMotionValue } from 'framer-motion/dom' 5 | export * from './components' 6 | export { default as LayoutGroup } from './components/LayoutGroup.vue' 7 | export { useLayoutGroup } from './components/use-layout-group' 8 | export type { LayoutGroupProps } from './components/use-layout-group' 9 | export * from './components/context' 10 | export * from './value' 11 | export * from './types' 12 | export * from './animation' 13 | export * from './utils' 14 | export { useDragControls } from './features/gestures/drag/use-drag-controls' 15 | export type { PanInfo } from './features/gestures/pan/PanSession' 16 | export { domAnimation, domMax } from '@/features' 17 | -------------------------------------------------------------------------------- /docs/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from 'class-variance-authority' 2 | 3 | export { default as Alert } from './Alert.vue' 4 | export { default as AlertDescription } from './AlertDescription.vue' 5 | export { default as AlertTitle } from './AlertTitle.vue' 6 | 7 | export const alertVariants = cva( 8 | 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-background text-foreground', 13 | destructive: 14 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: 'default', 19 | }, 20 | }, 21 | ) 22 | 23 | export type AlertVariants = VariantProps 24 | -------------------------------------------------------------------------------- /playground/vite/src/views/dynamic-variant.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 21 | Toggle 22 | 23 | 30 | CLICK ME 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/motion/src/value/use-computed.ts: -------------------------------------------------------------------------------- 1 | import { useCombineMotionValues } from '@/value/use-combine-values' 2 | import { type MotionValue, collectMotionValues } from 'framer-motion/dom' 3 | import { watchEffect } from 'vue' 4 | 5 | export function useComputed(computed: () => T): MotionValue { 6 | /** 7 | * Open session of collectMotionValues. Any MotionValue that calls get() 8 | * will be saved into this array. 9 | */ 10 | collectMotionValues.current = [] 11 | 12 | const { value, subscribe, unsubscribe, updateValue } = useCombineMotionValues(computed) 13 | 14 | subscribe(collectMotionValues.current) 15 | 16 | collectMotionValues.current = undefined 17 | 18 | watchEffect(() => { 19 | unsubscribe() 20 | collectMotionValues.current = [] 21 | updateValue() 22 | subscribe(collectMotionValues.current) 23 | collectMotionValues.current = undefined 24 | }) 25 | 26 | return value 27 | } 28 | -------------------------------------------------------------------------------- /docs/components/ui/scroll-area/ScrollArea.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/components/content/ButtonLink.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 21 | 25 | 30 | 31 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/motion/src/components/__tests__/motion-config.test.tsx: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' 2 | import { mount } from '@vue/test-utils' 3 | import { motionValue } from 'framer-motion/dom' 4 | import { defineComponent, nextTick } from 'vue' 5 | import MotionConfig from '@/components/motion-config/MotionConfig.vue' 6 | import { Motion } from '@/components/motion' 7 | 8 | describe('reducedMotion', () => { 9 | it('reducedMotion always', async () => { 10 | const scale = motionValue(1) 11 | const wrapper = mount(defineComponent({ 12 | setup() { 13 | return () => ( 14 | 15 | 16 | 17 | ) 18 | }, 19 | })) 20 | await nextTick() 21 | await new Promise(resolve => setTimeout(resolve, 20)) 22 | expect(scale.get()).toBe(0.5) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /docs/components/ui/command/Command.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /packages/motion/src/projection/utils/measure.ts: -------------------------------------------------------------------------------- 1 | import { convertBoundingBoxToBox, transformBoxPoints } from '@/projection/conversion' 2 | import { translateAxis } from '@/projection/geometry/delta-apply' 3 | import type { IProjectionNode, TransformPoint } from 'framer-motion' 4 | 5 | export function measureViewportBox( 6 | instance: HTMLElement, 7 | transformPoint?: TransformPoint, 8 | ) { 9 | return convertBoundingBoxToBox( 10 | transformBoxPoints(instance.getBoundingClientRect(), transformPoint), 11 | ) 12 | } 13 | 14 | export function measurePageBox( 15 | element: HTMLElement, 16 | rootProjectionNode: IProjectionNode, 17 | transformPagePoint?: TransformPoint, 18 | ) { 19 | const viewportBox = measureViewportBox(element, transformPagePoint) 20 | const { scroll } = rootProjectionNode 21 | 22 | if (scroll) { 23 | translateAxis(viewportBox.x, scroll.offset.x) 24 | translateAxis(viewportBox.y, scroll.offset.y) 25 | } 26 | 27 | return viewportBox 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | node_modules 3 | 4 | # Build outputs 5 | dist 6 | build 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Dependency directories 17 | jspm_packages/ 18 | 19 | # TypeScript cache 20 | *.tsbuildinfo 21 | 22 | # Optional npm cache directory 23 | .npm 24 | 25 | # Optional eslint cache 26 | .eslintcache 27 | 28 | # Optional REPL history 29 | .node_repl_history 30 | 31 | # Output of 'npm pack' 32 | *.tgz 33 | 34 | # Yarn Integrity file 35 | .yarn-integrity 36 | 37 | # dotenv environment variables file 38 | .env 39 | .env.test 40 | 41 | # Mac system files 42 | .DS_Store 43 | 44 | # Editor directories and files 45 | .idea/ 46 | .vscode/ 47 | *.suo 48 | *.ntvs* 49 | *.njsproj 50 | *.sln 51 | *.sw? 52 | docs/.vitepress/cache/**/* 53 | prompts 54 | .npmrc 55 | 56 | new-docs 57 | .cursorrules 58 | 59 | 60 | # Playwright 61 | playwright-report 62 | test-results 63 | .cursor 64 | -------------------------------------------------------------------------------- /playground/nuxt/pages/animate-present-initial.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 27 | 34 | {{ state }} 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/components/ui/command/CommandList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/content/4.motion-value/7. use-velocity.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useVelocity 3 | description: 4 | navigation.icon: 'lucide:chevrons-up' 5 | --- 6 | 7 | `useVelocity` accepts a motion value and returns a new one that updates with the provided motion value's velocity. 8 | 9 | ## Usage 10 | 11 | ```ts 12 | import { useVelocity } from 'motion-v' 13 | ``` 14 | 15 | > Pass any numerical motion value to useVelocity. It'll return a new motion value that updates with the velocity of the original value 16 | 17 | ```vue 18 | 28 | 29 | 30 | 31 | 32 | ``` 33 | 34 | ## Resources 35 | 36 | - [Motion React-useVelocity](https://motion.dev/docs/react-use-velocity) 37 | -------------------------------------------------------------------------------- /packages/motion/src/value/__tests__/use-transform.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import { mount } from '@vue/test-utils' 3 | import { Motion } from '@/components' 4 | import { defineComponent, nextTick, onMounted, ref, watchEffect } from 'vue' 5 | import { useTransform } from '@/value/use-transform' 6 | import { delay } from '@/shared/test' 7 | 8 | describe('useTransform', () => { 9 | it('should update when reactive value changes', async () => { 10 | const Component = defineComponent({ 11 | setup() { 12 | const x = ref(0) 13 | const transform = useTransform(() => { 14 | return x.value 15 | }) 16 | onMounted(() => { 17 | x.value = 100 18 | }) 19 | return () => { 20 | return 21 | } 22 | }, 23 | }) 24 | const wrapper = mount(Component) 25 | await delay(100) 26 | expect(wrapper.html()).toContain('100') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /playground/nuxt/demos/Animation.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | {{ show ? '隐藏' : '显示' }} 15 | 16 | 17 | 35 | 36 | 37 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /docs/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from 'class-variance-authority' 2 | 3 | export { default as Badge } from './Badge.vue' 4 | 5 | export const badgeVariants = cva( 6 | 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'default', 21 | }, 22 | }, 23 | ) 24 | 25 | export type BadgeVariants = VariantProps 26 | -------------------------------------------------------------------------------- /playground/vite/src/views/[...404].vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 6 | 7 | 8 | Page not found 9 | 10 | 14 | Go Home 15 | 16 | 17 | 18 | 19 | 20 | 42 | -------------------------------------------------------------------------------- /docs/components/demo/variants/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 36 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/content/4.motion-value/6.use-transform.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useTransform 3 | description: 4 | navigation.icon: 'lucide:rotate-3d' 5 | --- 6 | 7 | `useTransform` creates a new motion value that transforms the output of one or more motion values. 8 | 9 | ## Usage 10 | 11 | ```ts 12 | import { useTransform } from 'motion-v' 13 | ``` 14 | 15 | `useTransform` can be used in two ways: with a transform function and via value maps. 16 | 17 | ```ts 18 | // Transform function 19 | useTransform(() => x.get() * 2) 20 | 21 | // Value mapping 22 | useTransform(x, [0, 100], ['#f00', '00f']) 23 | ``` 24 | 25 | ## Drag transform 26 | 27 | 28 | 29 | ## HTML Content 30 | 31 | 32 | 33 | ::stackblitz 34 | --- 35 | src: https://stackblitz.com/edit/vitejs-vite-ff3czw?ctl=1&embed=1&file=src%2FApp.vue&hideExplorer=1 36 | title: motion-use-spring 37 | --- 38 | :: 39 | 40 | ## Resources 41 | 42 | - [Motion React-useTransform](https://motion.dev/docs/react-use-transform) 43 | -------------------------------------------------------------------------------- /docs/components/ui/navigation-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { cva } from 'class-variance-authority' 2 | 3 | export { default as NavigationMenu } from './NavigationMenu.vue' 4 | export { default as NavigationMenuContent } from './NavigationMenuContent.vue' 5 | export { default as NavigationMenuItem } from './NavigationMenuItem.vue' 6 | export { default as NavigationMenuLink } from './NavigationMenuLink.vue' 7 | export { default as NavigationMenuList } from './NavigationMenuList.vue' 8 | export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue' 9 | export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue' 10 | 11 | export const navigationMenuTriggerStyle = cva( 12 | 'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50', 13 | ) 14 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/in-view/types.ts: -------------------------------------------------------------------------------- 1 | import type { VariantType } from '@/types' 2 | import type { VariantLabels } from 'motion-dom' 3 | 4 | type MarginValue = `${number}${'px' | '%'}` 5 | 6 | type MarginType = MarginValue | `${MarginValue} ${MarginValue}` | `${MarginValue} ${MarginValue} ${MarginValue}` | `${MarginValue} ${MarginValue} ${MarginValue} ${MarginValue}` 7 | 8 | export interface InViewOptions { 9 | root?: Element | Document 10 | margin?: MarginType 11 | amount?: 'some' | 'all' | number 12 | } 13 | 14 | type ViewportEventHandler = (entry: IntersectionObserverEntry | null) => void 15 | 16 | export interface InViewProps { 17 | inViewOptions?: InViewOptions & { once?: boolean } 18 | /** 19 | * @deprecated Use `whileInView` instead. 20 | */ 21 | inView?: VariantLabels | VariantType 22 | /** 23 | * Variant to apply when the element is in view. 24 | */ 25 | whileInView?: VariantLabels | VariantType 26 | onViewportEnter?: ViewportEventHandler 27 | onViewportLeave?: ViewportEventHandler 28 | } 29 | -------------------------------------------------------------------------------- /docs/components/demo/unwrap-element/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | Toggle popover 11 | 12 | 13 | 14 | 18 | 24 | I'm a popover! 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /packages/motion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "jsx": "preserve", 5 | "lib": [ 6 | // Target ES2020 to align with Vite. 7 | // 8 | // Support for newer versions of language built-ins are 9 | // left for the users to include, because that would require: 10 | // - either the project doesn't need to support older versions of browsers; 11 | // - or the project has properly included the necessary polyfills. 12 | "ES2023", 13 | "DOM", 14 | "DOM.Iterable" 15 | // No `ScriptHost` because Vue 3 dropped support for IE 16 | ], 17 | "baseUrl": ".", 18 | "rootDir": "./src", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | }, 22 | // Set to empty to avoid accidental inclusion of unwanted types 23 | "types": ["node"], 24 | "outDir": "./dist/es" 25 | }, 26 | "include": ["./src/**/*.vue", "./src/**/*.ts", "./src/**/*.tsx"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /docs/components/ui/command/CommandItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/components/ui/navigation-menu/NavigationMenuIndicator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/components/demo/layout-group/ToggleContent.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 21 | 27 | {{ isOpen ? 'close' : 'open' }} 28 | 29 | 37 | {{ props.content }} 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/components/ui/navigation-menu/NavigationMenu.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/motion/src/value/use-velocity.ts: -------------------------------------------------------------------------------- 1 | import type { MotionValue } from 'framer-motion/dom' 2 | import { frame, motionValue } from 'framer-motion/dom' 3 | import { useMotionValueEvent } from '@/value/use-motion-value-event' 4 | 5 | /** 6 | * 创建一个 MotionValue,当提供的 MotionValue 速度变化时更新 7 | * 8 | * ```javascript 9 | * const x = useMotionValue(0) 10 | * const xVelocity = useVelocity(x) 11 | * const xAcceleration = useVelocity(xVelocity) 12 | * ``` 13 | * 14 | * @public 15 | */ 16 | export function useVelocity(value: MotionValue): MotionValue { 17 | const velocity = motionValue(value.getVelocity()) 18 | 19 | const updateVelocity = () => { 20 | const latest = value.getVelocity() 21 | velocity.set(latest) 22 | 23 | /** 24 | * 如果还有速度,安排下一帧继续检查直到速度为零 25 | */ 26 | if (latest) { 27 | frame.update(updateVelocity) 28 | } 29 | } 30 | 31 | useMotionValueEvent(value, 'change', () => { 32 | // 在当前帧结束时安排更新此值 33 | frame.update(updateVelocity, false, true) 34 | }) 35 | 36 | return velocity 37 | } 38 | -------------------------------------------------------------------------------- /playground/nuxt/demos/use-follow-pointer.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted } from 'vue' 2 | import { motionValue, useSpring } from 'motion-v' 3 | 4 | const spring = { damping: 3, stiffness: 50, restDelta: 0.001 } 5 | 6 | export function useFollowPointer(elementRef: Ref) { 7 | const xPoint = motionValue(0) 8 | const yPoint = motionValue(0) 9 | 10 | const x = useSpring(xPoint, spring) 11 | const y = useSpring(yPoint, spring) 12 | 13 | const handlePointerMove = ({ clientX, clientY }: MouseEvent) => { 14 | const element = elementRef.value.$el 15 | if (!element) 16 | return 17 | 18 | requestAnimationFrame(() => { 19 | xPoint.set(clientX - element.offsetLeft - element.offsetWidth / 2) 20 | yPoint.set(clientY - element.offsetTop - element.offsetHeight / 2) 21 | }) 22 | } 23 | 24 | onMounted(() => { 25 | window.addEventListener('pointermove', handlePointerMove) 26 | }) 27 | 28 | onUnmounted(() => { 29 | window.removeEventListener('pointermove', handlePointerMove) 30 | }) 31 | 32 | return { y, x } 33 | } 34 | -------------------------------------------------------------------------------- /packages/motion/src/features/gestures/drag/index.ts: -------------------------------------------------------------------------------- 1 | import { Feature } from '@/features/feature' 2 | import { VisualElementDragControls } from '@/features/gestures/drag/VisualElementDragControls' 3 | import { noop } from 'framer-motion/dom' 4 | 5 | export class DragGesture extends Feature { 6 | controls: VisualElementDragControls 7 | 8 | removeGroupControls: Function = noop 9 | removeListeners: Function = noop 10 | 11 | constructor(state) { 12 | super(state) 13 | this.controls = new VisualElementDragControls(state.visualElement) 14 | } 15 | 16 | mount() { 17 | // If we've been provided a DragControls for manual control over the drag gesture, 18 | // subscribe this component to it on mount. 19 | const { dragControls } = this.state.options 20 | 21 | if (dragControls) { 22 | this.removeGroupControls = dragControls.subscribe(this.controls) 23 | } 24 | this.removeListeners = this.controls.addListeners() || noop 25 | } 26 | 27 | unmount() { 28 | this.removeGroupControls() 29 | this.removeListeners() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/components/demo/multiple/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | {{ show ? 'hide' : 'show' }} 12 | 13 | 18 | 26 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/components/ui/command/CommandDialog.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/components/ui/drawer/DrawerContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /playground/nuxt/pages/in-view.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 24 | 25 | {{ item.text }} 26 | 27 | 28 | Scroll down to see more items animate in as they enter the viewport. Each item animates independently using the inView prop. 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/components/ui/scroll-area/ScrollBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/components/content/ProseImg.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | 42 | 43 | -------------------------------------------------------------------------------- /playground/nuxt/pages/layout.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | {{ isExpanded ? 'Shrink' : 'Expand' }} 20 | 21 | 22 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/components/ui/command/CommandGroup.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 24 | 28 | {{ heading }} 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/components/ui/navigation-menu/NavigationMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 28 | 29 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /packages/motion/src/features/animation/calc-child-stagger.ts: -------------------------------------------------------------------------------- 1 | import type { VisualElement } from 'framer-motion' 2 | import type { DynamicOption } from 'motion-dom' 3 | 4 | export function calcChildStagger( 5 | children: Set, 6 | child: VisualElement, 7 | delayChildren?: number | DynamicOption, 8 | staggerChildren: number = 0, 9 | staggerDirection: number = 1, 10 | ): number { 11 | const sortedChildren = Array.from(children) 12 | const index = sortedChildren.indexOf(child) 13 | const numChildren = children.size 14 | const maxStaggerDuration = (numChildren - 1) * staggerChildren 15 | const delayIsFunction = typeof delayChildren === 'function' 16 | /** 17 | * parent may not update, so we need to clear the enteringChildren when the child is the last one 18 | */ 19 | if (index === sortedChildren.length - 1) { 20 | child.parent.enteringChildren = undefined 21 | } 22 | return delayIsFunction 23 | ? delayChildren(index, numChildren) 24 | : staggerDirection === 1 25 | ? index * staggerChildren 26 | : maxStaggerDuration - index * staggerChildren 27 | } 28 | -------------------------------------------------------------------------------- /packages/motion/src/types/framer-motion.ts: -------------------------------------------------------------------------------- 1 | import type { MotionNodeOptions } from 'motion-dom' 2 | 3 | export type { Point } from 'framer-motion' 4 | 5 | export type SupportedEdgeUnit = 'px' | 'vw' | 'vh' | '%' 6 | 7 | export type EdgeUnit = `${number}${SupportedEdgeUnit}` 8 | 9 | export type NamedEdges = 'start' | 'end' | 'center' 10 | 11 | export type EdgeString = NamedEdges | EdgeUnit | `${number}` 12 | 13 | export type Edge = EdgeString | number 14 | 15 | export type ProgressIntersection = [number, number] 16 | 17 | export type Intersection = `${Edge} ${Edge}` 18 | 19 | export type ScrollOffset = Array 20 | 21 | export interface ScrollInfoOptions { 22 | container?: HTMLElement 23 | target?: Element 24 | axis?: 'x' | 'y' 25 | offset?: ScrollOffset 26 | } 27 | 28 | export interface Orchestration { 29 | delay?: number 30 | 31 | when?: false | 'beforeChildren' | 'afterChildren' | string 32 | delayChildren?: number 33 | 34 | staggerChildren?: number 35 | 36 | staggerDirection?: number 37 | } 38 | 39 | export type $Transition = MotionNodeOptions['transition'] 40 | -------------------------------------------------------------------------------- /docs/components/content/ProsePre.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 44 | 48 | 49 | 50 | 51 | 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rick Huang 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 | -------------------------------------------------------------------------------- /docs/content/2.components/5.reorder.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reorder 3 | description: 4 | navigation.icon: lucide:layout-grid 5 | 6 | --- 7 | 8 | The `Reorder` component is a component that allows you to drag to reorder items in a list. 9 | 10 | ## Basic Usage 11 | 12 | 13 | 14 | ## Layout Usage 15 | 16 | 17 | 18 | ## ReorderGroup Props 19 | 20 | ::PropsTable 21 | --- 22 | data: 23 | - name: axis 24 | default: "y" 25 | type: "'x' | 'y'" 26 | description: The axis to reorder on. To make draggable on both axes, set <ReorderItem drag /> 27 | - name: onUpdate:values 28 | default: "-" 29 | type: "(newOrder: V[]) => void" 30 | description: A callback to fire with the new value order dragging ends. 31 | - name: values 32 | default: "[]" 33 | type: "V[]" 34 | description: The latest values state. 35 | --- 36 | :: 37 | 38 | ## ReorderItem Props 39 | 40 | ::PropsTable 41 | --- 42 | data: 43 | - name: value 44 | type: "V" 45 | description: The value in the list that this component represents. 46 | --- 47 | :: 48 | -------------------------------------------------------------------------------- /packages/motion/src/components/group.ts: -------------------------------------------------------------------------------- 1 | import type { IProjectionNode } from 'framer-motion' 2 | 3 | function notify(node: IProjectionNode) { 4 | return !node.isLayoutDirty && node.willUpdate(false) 5 | } 6 | 7 | export interface NodeGroup { 8 | add: (node: IProjectionNode) => void 9 | remove: (node: IProjectionNode) => void 10 | dirty: VoidFunction 11 | } 12 | 13 | export function nodeGroup(): NodeGroup { 14 | const nodes = new Set() 15 | const subscriptions = new WeakMap void>() 16 | 17 | const dirtyAll = (node?: IProjectionNode) => { 18 | nodes.forEach(notify) 19 | } 20 | 21 | return { 22 | add: (node) => { 23 | nodes.add(node) 24 | subscriptions.set( 25 | node, 26 | node.addEventListener('willUpdate', () => dirtyAll(node)), 27 | ) 28 | }, 29 | remove: (node) => { 30 | nodes.delete(node) 31 | const unsubscribe = subscriptions.get(node) 32 | if (unsubscribe) { 33 | unsubscribe() 34 | subscriptions.delete(node) 35 | } 36 | dirtyAll(node) 37 | }, 38 | dirty: dirtyAll, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Minimal Starter 2 | 3 | Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | 19 | # bun 20 | bun install 21 | ``` 22 | 23 | ## Development Server 24 | 25 | Start the development server on `http://localhost:3000`: 26 | 27 | ```bash 28 | # npm 29 | npm run dev 30 | 31 | # pnpm 32 | pnpm dev 33 | 34 | # yarn 35 | yarn dev 36 | 37 | # bun 38 | bun run dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | # npm 47 | npm run build 48 | 49 | # pnpm 50 | pnpm build 51 | 52 | # yarn 53 | yarn build 54 | 55 | # bun 56 | bun run build 57 | ``` 58 | 59 | Locally preview production build: 60 | 61 | ```bash 62 | # npm 63 | npm run preview 64 | 65 | # pnpm 66 | pnpm preview 67 | 68 | # yarn 69 | yarn preview 70 | 71 | # bun 72 | bun run preview 73 | ``` 74 | 75 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 76 | -------------------------------------------------------------------------------- /docs/components/ui/tabs/TabsTrigger.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/components/content/Tabs.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/components/ui/separator/Separator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 28 | {{ props.label }} 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/components/ui/command/CommandInput.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 29 | 30 | 35 | 36 | 37 | --------------------------------------------------------------------------------
3 | 4 |
12 | 13 |
26 | 27 |
8 | Page not found 9 |
28 | Scroll down to see more items animate in as they enter the viewport. Each item animates independently using the inView prop. 29 |
<ReorderItem drag />