├── .eslintrc.json
├── .gitignore
├── .vercelignore
├── CONTRIBUTING.md
├── LICENCE.md
├── README.md
├── app
├── docs
│ ├── accordion
│ │ ├── accordion-basic.tsx
│ │ ├── accordion-icons.tsx
│ │ ├── accordion-variant.tsx
│ │ └── page.mdx
│ ├── animated-background
│ │ ├── animated-card-background-hover.tsx
│ │ ├── animated-tabs-hover.tsx
│ │ ├── animated-tabs.tsx
│ │ ├── page.mdx
│ │ └── segmented-control.tsx
│ ├── animated-group
│ │ ├── animated-group-custom-variants-2.tsx
│ │ ├── animated-group-custom-variants.tsx
│ │ ├── animated-group-preset.tsx
│ │ └── page.mdx
│ ├── animated-number
│ │ ├── animated-number-basic.tsx
│ │ ├── animated-number-counter.tsx
│ │ ├── animated-number-in-view.tsx
│ │ └── page.mdx
│ ├── border-trail
│ │ ├── border-trail-card-1.tsx
│ │ ├── border-trail-card-2.tsx
│ │ ├── border-trail-textarea.tsx
│ │ └── page.mdx
│ ├── carousel
│ │ ├── carousel-basic.tsx
│ │ ├── carousel-custom-indicator.tsx
│ │ ├── carousel-custom-sizes.tsx
│ │ ├── carousel-spacing.tsx
│ │ └── page.mdx
│ ├── cursor
│ │ ├── cursor-1.tsx
│ │ ├── cursor-2.tsx
│ │ ├── cursor-3.tsx
│ │ └── page.mdx
│ ├── dialog
│ │ ├── dialog-basic.tsx
│ │ ├── dialog-controlled.tsx
│ │ ├── dialog-custom-backdrop.tsx
│ │ ├── dialog-custom-exit.tsx
│ │ ├── dialog-custom-variants-transtion.tsx
│ │ └── page.mdx
│ ├── disclosure
│ │ ├── disclosure-basic.tsx
│ │ ├── disclosure-card.tsx
│ │ └── page.mdx
│ ├── dock
│ │ ├── apple-style-dock.tsx
│ │ └── page.mdx
│ ├── glow-effect
│ │ ├── glow-effect-button.tsx
│ │ ├── glow-effect-card-background.tsx
│ │ ├── glow-effect-card-mode.tsx
│ │ └── page.mdx
│ ├── image-comparison
│ │ ├── image-comparison-basic.tsx
│ │ ├── image-comparison-custom-slider.tsx
│ │ ├── image-comparison-hover.tsx
│ │ ├── image-comparison-spring.tsx
│ │ └── page.mdx
│ ├── in-view
│ │ ├── in-view-basic-multiple.tsx
│ │ ├── in-view-basic.tsx
│ │ ├── in-view-images-grid.tsx
│ │ └── page.mdx
│ ├── infinite-slider
│ │ ├── infinite-slider-basic.tsx
│ │ ├── infinite-slider-hover-speed.tsx
│ │ ├── infinite-slider-vertical.tsx
│ │ └── page.mdx
│ ├── installation
│ │ └── page.mdx
│ ├── layout.tsx
│ ├── magnetic
│ │ ├── magnetic-basic.tsx
│ │ ├── magnetic-nested.tsx
│ │ └── page.mdx
│ ├── morphing-dialog
│ │ ├── morphing-dialog-basic-1.tsx
│ │ ├── morphing-dialog-basic-2.tsx
│ │ ├── morphing-dialog-image.tsx
│ │ └── page.mdx
│ ├── morphing-popover
│ │ ├── morphing-popover-basic.tsx
│ │ ├── morphing-popover-custom-transition-variants.tsx
│ │ ├── morphing-popover-textarea.tsx
│ │ └── page.mdx
│ ├── navigation.ts
│ ├── page.mdx
│ ├── progressive-blur
│ │ ├── page.mdx
│ │ ├── progressive-blur-basic.tsx
│ │ ├── progressive-blur-hover.tsx
│ │ └── progressive-blur-slider.tsx
│ ├── scroll-progress
│ │ ├── page.mdx
│ │ ├── scroll-progress-basic-1.tsx
│ │ ├── scroll-progress-basic-2.tsx
│ │ └── scroll-progress-basic-3.tsx
│ ├── sliding-number
│ │ ├── clock.tsx
│ │ ├── page.mdx
│ │ ├── sliding-basic.tsx
│ │ └── sliding-slider.tsx
│ ├── spinning-text
│ │ ├── page.mdx
│ │ ├── spinning-text-basic.tsx
│ │ ├── spinning-text-custom-transition.tsx
│ │ └── spinning-text-custom-variants.tsx
│ ├── spotlight
│ │ ├── page.mdx
│ │ ├── spotlight-basic.tsx
│ │ ├── spotlight-border.tsx
│ │ └── spotlight-custom-color.tsx
│ ├── text-effect
│ │ ├── page.mdx
│ │ ├── text-effect-custom-delay.tsx
│ │ ├── text-effect-exit.tsx
│ │ ├── text-effect-line.tsx
│ │ ├── text-effect-per-char.tsx
│ │ ├── text-effect-per-word.tsx
│ │ ├── text-effect-preset.tsx
│ │ ├── text-effect-speed.tsx
│ │ └── text-effect-variants.tsx
│ ├── text-loop
│ │ ├── page.mdx
│ │ ├── text-loop-basic.tsx
│ │ ├── text-loop-custom-variants-transition.tsx
│ │ └── text-loop-on-index.tsx
│ ├── text-morph
│ │ ├── page.mdx
│ │ ├── text-morph-button.tsx
│ │ └── text-morph-input.tsx
│ ├── text-roll
│ │ ├── page.mdx
│ │ ├── text-roll-basic.tsx
│ │ ├── text-roll-custom-transition-delay.tsx
│ │ └── text-roll-custom-variants.tsx
│ ├── text-scramble
│ │ ├── page.mdx
│ │ ├── text-scramble-basic.tsx
│ │ ├── text-scramble-custom-char-duration.tsx
│ │ └── text-scramble-custom-trigger.tsx
│ ├── text-shimmer-wave
│ │ ├── page.mdx
│ │ ├── text-shimmer-wave-basic.tsx
│ │ └── text-shimmer-wave-color.tsx
│ ├── text-shimmer
│ │ ├── page.mdx
│ │ ├── text-shimmer-basic.tsx
│ │ └── text-shimmer-color.tsx
│ ├── tilt
│ │ ├── page.mdx
│ │ ├── tilt-card-1.tsx
│ │ └── tilt-spotlight.tsx
│ ├── toolbar-dynamic
│ │ └── page.mdx
│ ├── toolbar-expandable
│ │ └── page.mdx
│ └── transition-panel
│ │ ├── page.mdx
│ │ ├── transition-panel-card.tsx
│ │ └── transition-panel-tabs.tsx
├── globals.css
├── icon.ico
├── layout.tsx
├── opengraph-image.alt
├── opengraph-image.jpg
├── page.tsx
├── showcase
│ └── page.tsx
├── twitter-image.alt
└── twitter-image.jpg
├── cli
├── README.md
├── package-lock.json
├── package.json
├── src
│ └── index.ts
└── tsconfig.json
├── components.json
├── components
├── core
│ ├── accordion.tsx
│ ├── animated-background.tsx
│ ├── animated-group.tsx
│ ├── animated-number.tsx
│ ├── border-trail.tsx
│ ├── carousel.tsx
│ ├── cursor.tsx
│ ├── dialog.tsx
│ ├── disclosure.tsx
│ ├── dock.tsx
│ ├── glow-effect.tsx
│ ├── image-comparison.tsx
│ ├── in-view.tsx
│ ├── infinite-slider.tsx
│ ├── magnetic.tsx
│ ├── morphing-dialog.tsx
│ ├── morphing-popover.tsx
│ ├── progressive-blur.tsx
│ ├── scroll-progress.tsx
│ ├── sliding-number.tsx
│ ├── spinning-text.tsx
│ ├── spotlight.tsx
│ ├── text-effect.tsx
│ ├── text-loop.tsx
│ ├── text-morph.tsx
│ ├── text-roll.tsx
│ ├── text-scramble.tsx
│ ├── text-shimmer-wave.tsx
│ ├── text-shimmer.tsx
│ ├── tilt.tsx
│ ├── toolbar-dynamic.tsx
│ ├── toolbar-expandable.tsx
│ └── transition-panel.tsx
├── ui
│ ├── button.tsx
│ ├── input.tsx
│ ├── label.tsx
│ └── tooltip.tsx
└── website
│ ├── card-example-landing.tsx
│ ├── code-block.tsx
│ ├── code-preview.tsx
│ ├── code-renderer.tsx
│ ├── component-code-preview.tsx
│ ├── component-preview.tsx
│ ├── dropdown-menu.tsx
│ ├── header.tsx
│ ├── icons
│ ├── github.tsx
│ ├── motion-primitives-logo.tsx
│ ├── shadcn-logo.tsx
│ └── x.tsx
│ ├── installation-cli.tsx
│ ├── launch-banner.tsx
│ ├── open-in-v0.tsx
│ ├── scroll-area.tsx
│ ├── table-of-contents.tsx
│ ├── tabs.tsx
│ ├── theme-provider.tsx
│ └── theme-switch.tsx
├── hooks
├── useClickOutside.tsx
└── usePreventScroll.tsx
├── lib
├── browser.ts
├── code.ts
├── custom-theme.ts
├── shiki.ts
├── theme-css-variables.ts
└── utils.ts
├── mdx-components.tsx
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prettier.config.js
├── public
├── apple_music_logo.svg
├── c
│ ├── accordion.json
│ ├── animated-background.json
│ ├── animated-group.json
│ ├── animated-number.json
│ ├── border-trail.json
│ ├── carousel.json
│ ├── cursor.json
│ ├── dialog.json
│ ├── disclosure.json
│ ├── dock.json
│ ├── glow-effect.json
│ ├── image-comparison.json
│ ├── in-view.json
│ ├── infinite-slider.json
│ ├── magnetic.json
│ ├── morphing-dialog.json
│ ├── morphing-popover.json
│ ├── progressive-blur.json
│ ├── registry.json
│ ├── scroll-progress.json
│ ├── sliding-number.json
│ ├── spinning-text.json
│ ├── spotlight.json
│ ├── text-effect.json
│ ├── text-loop.json
│ ├── text-morph.json
│ ├── text-roll.json
│ ├── text-scramble.json
│ ├── text-shimmer-wave.json
│ ├── text-shimmer.json
│ ├── tilt.json
│ ├── toolbar-dynamic.json
│ ├── toolbar-expandable.json
│ └── transition-panel.json
├── chrome_logo.svg
├── e
│ ├── accordion-basic.json
│ ├── accordion-icons.json
│ ├── accordion-variant.json
│ ├── animated-card-background-hover.json
│ ├── animated-group-custom-variants-2.json
│ ├── animated-group-custom-variants.json
│ ├── animated-group-preset.json
│ ├── animated-number-basic.json
│ ├── animated-number-counter.json
│ ├── animated-number-in-view.json
│ ├── animated-tabs-hover.json
│ ├── animated-tabs.json
│ ├── apple-style-dock.json
│ ├── border-trail-card-1.json
│ ├── border-trail-card-2.json
│ ├── border-trail-textarea.json
│ ├── carousel-basic.json
│ ├── carousel-custom-indicator.json
│ ├── carousel-custom-sizes.json
│ ├── carousel-spacing.json
│ ├── clock.json
│ ├── cursor-1.json
│ ├── cursor-2.json
│ ├── cursor-3.json
│ ├── dialog-basic.json
│ ├── dialog-controlled.json
│ ├── dialog-custom-backdrop.json
│ ├── dialog-custom-exit.json
│ ├── dialog-custom-variants-transtion.json
│ ├── disclosure-basic.json
│ ├── disclosure-card.json
│ ├── glow-effect-button.json
│ ├── glow-effect-card-background.json
│ ├── glow-effect-card-mode.json
│ ├── image-comparison-basic.json
│ ├── image-comparison-custom-slider.json
│ ├── image-comparison-hover.json
│ ├── image-comparison-spring.json
│ ├── in-view-basic-multiple.json
│ ├── in-view-basic.json
│ ├── in-view-images-grid.json
│ ├── infinite-slider-basic.json
│ ├── infinite-slider-hover-speed.json
│ ├── infinite-slider-vertical.json
│ ├── magnetic-basic.json
│ ├── magnetic-nested.json
│ ├── morphing-dialog-basic-1.json
│ ├── morphing-dialog-basic-2.json
│ ├── morphing-dialog-image.json
│ ├── morphing-popover-basic.json
│ ├── morphing-popover-custom-transition-variants.json
│ ├── morphing-popover-textarea.json
│ ├── progressive-blur-basic.json
│ ├── progressive-blur-hover.json
│ ├── progressive-blur-slider.json
│ ├── scroll-progress-basic-1.json
│ ├── scroll-progress-basic-2.json
│ ├── scroll-progress-basic-3.json
│ ├── segmented-control.json
│ ├── sliding-basic.json
│ ├── sliding-slider.json
│ ├── spinning-text-basic.json
│ ├── spinning-text-custom-transition.json
│ ├── spinning-text-custom-variants.json
│ ├── spotlight-basic.json
│ ├── spotlight-border.json
│ ├── spotlight-custom-color.json
│ ├── text-effect-custom-delay.json
│ ├── text-effect-exit.json
│ ├── text-effect-line.json
│ ├── text-effect-per-char.json
│ ├── text-effect-per-word.json
│ ├── text-effect-preset.json
│ ├── text-effect-speed.json
│ ├── text-effect-variants.json
│ ├── text-loop-basic.json
│ ├── text-loop-custom-variants-transition.json
│ ├── text-loop-on-index.json
│ ├── text-morph-button.json
│ ├── text-morph-input.json
│ ├── text-roll-basic.json
│ ├── text-roll-custom-transition-delay.json
│ ├── text-roll-custom-variants.json
│ ├── text-scramble-basic.json
│ ├── text-scramble-custom-char-duration.json
│ ├── text-scramble-custom-trigger.json
│ ├── text-shimmer-basic.json
│ ├── text-shimmer-color.json
│ ├── text-shimmer-wave-basic.json
│ ├── text-shimmer-wave-color.json
│ ├── tilt-card-1.json
│ ├── tilt-spotlight.json
│ ├── transition-panel-card.json
│ └── transition-panel-tabs.json
├── eb-27-lamp-edouard-wilfrid-buquet.jpg
├── gucci_logo.svg
├── h
│ ├── useClickOutside.json
│ └── usePreventScroll.json
├── jquery_logo.svg
├── mp_dark.png
├── mp_light.png
├── national_geographic_logo.svg
├── nintendo_logo.svg
├── prada_logo.svg
├── sony_logo.svg
└── strava_logo.svg
├── scripts
├── registry-build.ts
├── registry-components.ts
├── registry-examples.ts
├── registry-hooks.ts
└── registry-schema.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "prettier",
5 | "plugin:tailwindcss/recommended"
6 | ],
7 | "plugins": ["tailwindcss"]
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # cli
39 | cli/dist
40 | cli/node_modules
41 |
--------------------------------------------------------------------------------
/.vercelignore:
--------------------------------------------------------------------------------
1 | /cli
2 |
--------------------------------------------------------------------------------
/LICENCE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 ibelick
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 | app/page.tsx
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Motion-Primitives
2 |
3 | Beautifully designed, easy-to-integrate motion components for engineers and designers, built with [motion](https://motion.dev/) and [Tailwind CSS](https://tailwindcss.com/).
4 |
5 | **This project is in beta. Expect new components to be released regularly and significant updates to the code.**
6 |
7 |
8 |
9 |
10 |
11 | ## Documentation
12 |
13 | Visit [motion-primitives.com/docs](http://motion-primitives.com/docs) to view the full documentation.
14 |
15 | ## Contributing
16 |
17 | Please read the [contributing guide](/CONTRIBUTING.md).
18 |
19 | ## License
20 |
21 | Licensed under the [MIT license](/LICENSE.md).
22 |
--------------------------------------------------------------------------------
/app/docs/animated-background/animated-card-background-hover.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatedBackground } from '@/components/core/animated-background';
2 |
3 | export function AnimatedCardBackgroundHover() {
4 | const ITEMS = [
5 | {
6 | id: 1,
7 | title: 'Dialog',
8 | description: 'Enhances modal presentations.',
9 | },
10 | {
11 | id: 2,
12 | title: 'Popover',
13 | description: 'For small interactive overlays.',
14 | },
15 | {
16 | id: 3,
17 | title: 'Accordion',
18 | description: 'Collapsible sections for more content.',
19 | },
20 | {
21 | id: 4,
22 | title: 'Collapsible',
23 | description: 'Collapsible sections for more content.',
24 | },
25 | {
26 | id: 5,
27 | title: 'Drag to Reorder',
28 | description: 'Reorder items with drag and drop.',
29 | },
30 | {
31 | id: 6,
32 | title: 'Swipe to Delete',
33 | description: 'Delete items with swipe gestures.',
34 | },
35 | ];
36 |
37 | return (
38 |
39 |
48 | {ITEMS.map((item, index) => (
49 |
50 |
51 |
52 | {item.title}
53 |
54 |
55 | {item.description}
56 |
57 |
58 |
59 | ))}
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/app/docs/animated-background/animated-tabs-hover.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatedBackground } from '@/components/core/animated-background';
2 |
3 | export function AnimatedTabsHover() {
4 | const TABS = ['Home', 'About', 'Services', 'Contact'];
5 |
6 | return (
7 |
8 |
18 | {TABS.map((tab, index) => (
19 |
25 | {tab}
26 |
27 | ))}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/docs/animated-background/animated-tabs.tsx:
--------------------------------------------------------------------------------
1 | import { Home, PhoneCall, Settings, User } from 'lucide-react';
2 | import { AnimatedBackground } from '@/components/core/animated-background';
3 |
4 | export function AnimatedTabs() {
5 | const TABS = [
6 | {
7 | label: 'Home',
8 | icon: ,
9 | },
10 | {
11 | label: 'About',
12 | icon: ,
13 | },
14 | {
15 | label: 'Services',
16 | icon: ,
17 | },
18 | {
19 | label: 'Contact',
20 | icon: ,
21 | },
22 | ];
23 |
24 | return (
25 |
26 |
27 |
36 | {TABS.map((tab) => (
37 |
43 | {tab.icon}
44 |
45 | ))}
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/docs/animated-background/segmented-control.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatedBackground } from '@/components/core/animated-background';
2 |
3 | export function SegmentedControl() {
4 | return (
5 |
6 |
14 | {['Day', 'Week', 'Month', 'Year'].map((label, index) => {
15 | return (
16 |
23 | {label}
24 |
25 | );
26 | })}
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/app/docs/animated-group/animated-group-custom-variants-2.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { AnimatedGroup } from '@/components/core/animated-group';
3 |
4 | export function AnimatedGroupCustomVariants2() {
5 | return (
6 |
37 |
38 |
43 |
44 |
45 |
46 |
47 |
48 |
53 |
54 |
55 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/app/docs/animated-group/animated-group-custom-variants.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatedGroup } from '@/components/core/animated-group';
2 |
3 | export function AnimatedGroupCustomVariants() {
4 | return (
5 |
32 |
37 |
42 |
47 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/app/docs/animated-group/animated-group-preset.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatedGroup } from '@/components/core/animated-group';
2 |
3 | export function AnimatedGroupPreset() {
4 | return (
5 |
9 |
14 |
19 |
24 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/docs/animated-number/animated-number-basic.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect, useState } from 'react';
3 | import { AnimatedNumber } from '@/components/core/animated-number';
4 |
5 | export function AnimatedNumberBasic() {
6 | const [value, setValue] = useState(0);
7 |
8 | useEffect(() => {
9 | setValue(2082);
10 | }, []);
11 |
12 | return (
13 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/docs/animated-number/animated-number-counter.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState } from 'react';
3 | import { AnimatedNumber } from '@/components/core/animated-number';
4 | import { Minus, Plus } from 'lucide-react';
5 |
6 | export function AnimatedNumberCounter() {
7 | const [value, setValue] = useState(1000);
8 |
9 | return (
10 |
11 |
setValue((prev) => prev - 100)}
14 | >
15 |
16 |
17 |
25 |
setValue((prev) => prev + 100)}
28 | >
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/docs/animated-number/animated-number-in-view.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { AnimatedNumber } from '@/components/core/animated-number';
3 | import { useInView } from 'motion/react';
4 | import { useRef, useState } from 'react';
5 |
6 | export function AnimatedNumberInView() {
7 | const [value, setValue] = useState(0);
8 | const ref = useRef(null);
9 | const isInView = useInView(ref);
10 |
11 | if (isInView && value === 0) {
12 | setValue(10000);
13 | }
14 |
15 | return (
16 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/app/docs/border-trail/border-trail-card-1.tsx:
--------------------------------------------------------------------------------
1 | import { BorderTrail } from '@/components/core/border-trail';
2 |
3 | export function BorderTrailCard1() {
4 | return (
5 |
6 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/docs/border-trail/border-trail-card-2.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import { BorderTrail } from '@/components/core/border-trail';
4 | import { useState } from 'react';
5 |
6 | export function BorderTrailCard2() {
7 | const [isLoading, setIsLoading] = useState(false);
8 | const [isVisible, setIsVisible] = useState(true);
9 |
10 | const handleAnimationComplete = () => {
11 | setIsLoading(false);
12 | setTimeout(() => setIsVisible(false), 300);
13 | };
14 |
15 | const handleLoad = () => {
16 | setIsLoading(true);
17 | setIsVisible(true);
18 | };
19 |
20 | return (
21 |
22 | {isVisible && (
23 |
36 | )}
37 |
38 |
44 | Submit
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/app/docs/border-trail/border-trail-textarea.tsx:
--------------------------------------------------------------------------------
1 | import { BorderTrail } from '@/components/core/border-trail';
2 |
3 | export function BorderTrailTextarea() {
4 | return (
5 |
6 |
7 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/docs/border-trail/page.mdx:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | title: 'Border Trail - Motion-Primitives',
3 | description:
4 | 'Animated border effect that moves along the edges of its parent container.',
5 | };
6 |
7 | import { BorderTrailCard1 } from './border-trail-card-1';
8 | import { BorderTrailCard2 } from './border-trail-card-2';
9 | import { BorderTrailTextarea } from './border-trail-textarea';
10 | import ComponentCodePreview from '@/components/website/component-code-preview';
11 |
12 | # Border Trail
13 |
14 | Animated border effect that moves along the edges of its parent container.
15 |
16 | ## Examples
17 |
18 | ### Border Trail Card
19 |
20 | }
22 | filePath='app/docs/border-trail/border-trail-card-1.tsx'
23 | />
24 |
25 | ### Border Trail Loading Card
26 |
27 | }
29 | filePath='app/docs/border-trail/border-trail-card-2.tsx'
30 | />
31 |
32 | ### Border Trail Textarea
33 |
34 | }
36 | filePath='app/docs/border-trail/border-trail-textarea.tsx'
37 | />
38 |
39 | ## Installation
40 |
41 |
42 |
43 |
44 | CLI
45 | Manual
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Copy and paste the following code into your project.
59 |
60 |
61 |
62 | Update the import paths to match your project setup.
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ## Component API
71 |
72 | ### Border Trail
73 |
74 | | Prop | Type | Default | Description |
75 | | :------------------ | :--------- | :------ | :-------------------------------------------------------- |
76 | | className | string | | Additional CSS classes for styling the border trail. |
77 | | size | number | 60 | Size of the animated border effect. |
78 | | transition | Transition | | Custom transition settings for the animation. |
79 | | onAnimationComplete | () => void | | Callback function triggered when the animation completes. |
80 |
--------------------------------------------------------------------------------
/app/docs/carousel/carousel-basic.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Carousel,
3 | CarouselContent,
4 | CarouselNavigation,
5 | CarouselIndicator,
6 | CarouselItem,
7 | } from '@/components/core/carousel';
8 |
9 | export function CarouselBasic() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | 1
17 |
18 |
19 |
20 |
21 | 2
22 |
23 |
24 |
25 |
26 | 3
27 |
28 |
29 |
30 |
31 | 4
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/docs/carousel/carousel-custom-indicator.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import {
3 | Carousel,
4 | CarouselContent,
5 | CarouselItem,
6 | } from '@/components/core/carousel';
7 | import { useState } from 'react';
8 |
9 | const ITEMS = new Array(4).fill(null).map((_, index) => index + 1);
10 |
11 | export function CarouselCustomIndicator() {
12 | const [index, setIndex] = useState(0);
13 |
14 | return (
15 |
16 |
17 |
18 | {ITEMS.map((item) => {
19 | return (
20 |
21 |
22 | {item}
23 |
24 |
25 | );
26 | })}
27 |
28 |
29 |
30 | {ITEMS.map((item) => {
31 | return (
32 | setIndex(item - 1)}
37 | className='h-12 w-12 border border-zinc-200 dark:border-zinc-800'
38 | >
39 | {item}
40 |
41 | );
42 | })}
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/app/docs/carousel/carousel-custom-sizes.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Carousel,
3 | CarouselContent,
4 | CarouselNavigation,
5 | CarouselItem,
6 | } from '@/components/core/carousel';
7 |
8 | export function CarouselCustomSizes() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | 1
16 |
17 |
18 |
19 |
20 | 2
21 |
22 |
23 |
24 |
25 | 3
26 |
27 |
28 |
29 |
30 | 4
31 |
32 |
33 |
34 |
35 | 5
36 |
37 |
38 |
39 |
40 | 6
41 |
42 |
43 |
44 |
45 | 7
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/app/docs/carousel/carousel-spacing.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Carousel,
3 | CarouselContent,
4 | CarouselNavigation,
5 | CarouselItem,
6 | } from '@/components/core/carousel';
7 |
8 | export function CarouselSpacing() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | 1
16 |
17 |
18 |
19 |
20 | 2
21 |
22 |
23 |
24 |
25 | 3
26 |
27 |
28 |
29 |
30 | 4
31 |
32 |
33 |
34 |
35 | 5
36 |
37 |
38 |
39 |
40 | 6
41 |
42 |
43 |
44 |
45 | 7
46 |
47 |
48 |
49 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/docs/cursor/cursor-2.tsx:
--------------------------------------------------------------------------------
1 | import { SVGProps } from 'react';
2 | import { Cursor } from '@/components/core/cursor';
3 |
4 | const MouseIcon = (props: SVGProps) => {
5 | return (
6 |
13 |
14 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export function Cursor2() {
34 | return (
35 |
36 |
37 |
50 |
51 |
52 |
53 | The city below
54 |
55 |
56 |
57 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/app/docs/cursor/cursor-3.tsx:
--------------------------------------------------------------------------------
1 | import { Cursor } from '@/components/core/cursor';
2 |
3 | export function Cursor3() {
4 | return (
5 |
6 |
7 |
24 |
29 |
30 | Christian Church, Eastern Europe
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/app/docs/dialog/dialog-basic.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogDescription,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogTrigger,
8 | DialogClose,
9 | } from '@/components/core/dialog';
10 |
11 | export function DialogBasic() {
12 | return (
13 |
14 |
15 | Join the waitlist
16 |
17 |
18 |
19 |
20 | Join the waitlist
21 |
22 |
23 | Enter your email address to receive updates when we launch.
24 |
25 |
26 |
27 |
28 | Email
29 |
30 |
36 |
40 | Join now
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/app/docs/dialog/dialog-controlled.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import {
3 | Dialog,
4 | DialogDescription,
5 | DialogContent,
6 | DialogHeader,
7 | DialogTitle,
8 | DialogClose,
9 | } from '@/components/core/dialog';
10 | import { useState } from 'react';
11 |
12 | export function DialogControlled() {
13 | const [isOpen, setIsOpen] = useState(false);
14 |
15 | return (
16 |
17 |
setIsOpen(true)} type='button' className='text-sm p-2'>
18 | Open Dialog
19 |
20 |
21 |
22 |
23 |
24 | Join the waitlist
25 |
26 |
27 | Enter your email address to receive updates when we launch.
28 |
29 |
30 |
31 |
32 | Email
33 |
34 |
40 |
44 | Join now
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/app/docs/dialog/dialog-custom-backdrop.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogDescription,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogClose,
8 | DialogTrigger,
9 | } from '@/components/core/dialog';
10 |
11 | export function DialogCustomBackdrop() {
12 | return (
13 |
14 |
15 | Join the waitlist
16 |
17 |
18 |
19 |
20 | Join the waitlist
21 |
22 |
23 | Enter your email address to receive updates when we launch.
24 |
25 |
26 |
27 |
28 | Email
29 |
30 |
36 |
40 | Join now
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/app/docs/dialog/dialog-custom-exit.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogDescription,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogTrigger,
8 | DialogClose,
9 | } from '@/components/core/dialog';
10 | import { Variants, Transition } from 'motion/react';
11 |
12 | export function DialogCustomExit() {
13 | const customVariants: Variants = {
14 | initial: {
15 | opacity: 0,
16 | scale: 0.95,
17 | y: 40,
18 | },
19 | animate: {
20 | opacity: 1,
21 | scale: 1,
22 | y: 0,
23 | },
24 | exit: {
25 | opacity: 0,
26 | scale: 0.95,
27 | y: 40,
28 | },
29 | };
30 |
31 | const customTransition: Transition = {
32 | type: 'spring',
33 | bounce: 0,
34 | duration: 0.25,
35 | };
36 |
37 | return (
38 |
39 |
40 | Join the waitlist
41 |
42 |
43 |
44 |
45 | Join the waitlist
46 |
47 |
48 | Enter your email address to receive updates when we launch.
49 |
50 |
51 |
52 |
53 | Email
54 |
55 |
61 |
65 | Join now
66 |
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/app/docs/dialog/dialog-custom-variants-transtion.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogDescription,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogTrigger,
8 | DialogClose,
9 | } from '@/components/core/dialog';
10 | import { Variants, Transition } from 'motion/react';
11 |
12 | export function DialogCustomVariantsTransition() {
13 | const customVariants: Variants = {
14 | initial: {
15 | scale: 0.9,
16 | filter: 'blur(10px)',
17 | y: '100%',
18 | },
19 | animate: {
20 | scale: 1,
21 | filter: 'blur(0px)',
22 | y: 0,
23 | },
24 | };
25 |
26 | const customTransition: Transition = {
27 | type: 'spring',
28 | bounce: 0,
29 | duration: 0.4,
30 | };
31 |
32 | return (
33 |
34 |
35 | Join the waitlist
36 |
37 |
38 |
39 |
40 | Join the waitlist
41 |
42 |
43 | Enter your email address to receive updates when we launch.
44 |
45 |
46 |
47 |
48 | Email
49 |
50 |
56 |
60 | Join now
61 |
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/app/docs/disclosure/disclosure-basic.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Disclosure,
3 | DisclosureContent,
4 | DisclosureTrigger,
5 | } from '@/components/core/disclosure';
6 |
7 | export function DisclosureBasic() {
8 | return (
9 |
10 |
11 |
12 | Show more
13 |
14 |
15 |
16 |
17 |
18 |
19 | This example demonstrates how you can use{' '}
20 | Disclosure component.
21 |
22 |
23 | {`function DisclosureBasic() {\n return (\n \n \n \n Show more\n \n \n \n hey
\n \n \n );`}
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/app/docs/dock/apple-style-dock.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Activity,
3 | Component,
4 | HomeIcon,
5 | Mail,
6 | Package,
7 | ScrollText,
8 | SunMoon,
9 | } from 'lucide-react';
10 |
11 | import { Dock, DockIcon, DockItem, DockLabel } from '@/components/core/dock';
12 |
13 | const data = [
14 | {
15 | title: 'Home',
16 | icon: (
17 |
18 | ),
19 | href: '#',
20 | },
21 | {
22 | title: 'Products',
23 | icon: (
24 |
25 | ),
26 | href: '#',
27 | },
28 | {
29 | title: 'Components',
30 | icon: (
31 |
32 | ),
33 | href: '#',
34 | },
35 | {
36 | title: 'Activity',
37 | icon: (
38 |
39 | ),
40 | href: '#',
41 | },
42 | {
43 | title: 'Change Log',
44 | icon: (
45 |
46 | ),
47 | href: '#',
48 | },
49 | {
50 | title: 'Email',
51 | icon: (
52 |
53 | ),
54 | href: '#',
55 | },
56 | {
57 | title: 'Theme',
58 | icon: (
59 |
60 | ),
61 | href: '#',
62 | },
63 | ];
64 |
65 | export function AppleStyleDock() {
66 | return (
67 |
68 |
69 | {data.map((item, idx) => (
70 |
74 | {item.title}
75 | {item.icon}
76 |
77 | ))}
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/app/docs/glow-effect/glow-effect-button.tsx:
--------------------------------------------------------------------------------
1 | import { GlowEffect } from '@/components/core/glow-effect';
2 | import { ArrowRight } from 'lucide-react';
3 |
4 | export function GlowEffectButton() {
5 | return (
6 |
7 |
14 |
15 | Explore
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/app/docs/glow-effect/glow-effect-card-background.tsx:
--------------------------------------------------------------------------------
1 | import { GlowEffect } from '@/components/core/glow-effect';
2 |
3 | export function GlowEffectCardBackground() {
4 | return (
5 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/app/docs/glow-effect/glow-effect-card-mode.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { motion } from 'motion/react';
3 | import { TextMorph } from '@/components/core/text-morph';
4 | import { GlowEffect } from '@/components/core/glow-effect';
5 | import { useState } from 'react';
6 |
7 | export function GlowEffectCardMode() {
8 | const [isVisible, setIsVisible] = useState(false);
9 |
10 | const handleLoad = () => {
11 | if (isVisible) {
12 | setIsVisible(false);
13 | return;
14 | }
15 |
16 | setIsVisible(true);
17 | };
18 |
19 | return (
20 |
21 |
31 |
37 |
38 |
39 |
45 | {isVisible ? 'Submitting...' : 'Submit'}
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/docs/image-comparison/image-comparison-basic.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ImageComparison,
3 | ImageComparisonImage,
4 | ImageComparisonSlider,
5 | } from '@/components/core/image-comparison';
6 |
7 | export function ImageComparisonBasic() {
8 | return (
9 |
10 |
15 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/docs/image-comparison/image-comparison-custom-slider.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ImageComparison,
3 | ImageComparisonImage,
4 | ImageComparisonSlider,
5 | } from '@/components/core/image-comparison';
6 |
7 | export function ImageComparisonCustomSlider() {
8 | return (
9 |
10 |
15 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/app/docs/image-comparison/image-comparison-hover.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ImageComparison,
3 | ImageComparisonImage,
4 | ImageComparisonSlider,
5 | } from '@/components/core/image-comparison';
6 |
7 | export function ImageComparisonHover() {
8 | return (
9 |
13 |
18 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/docs/image-comparison/image-comparison-spring.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ImageComparison,
3 | ImageComparisonImage,
4 | ImageComparisonSlider,
5 | } from '@/components/core/image-comparison';
6 |
7 | export function ImageComparisonSpring() {
8 | return (
9 |
16 |
21 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/app/docs/in-view/in-view-basic.tsx:
--------------------------------------------------------------------------------
1 | import { InView } from '@/components/core/in-view';
2 |
3 | export function InViewBasic() {
4 | return (
5 |
6 |
Scroll down
7 |
8 |
16 |
17 |
18 |
19 | Craft beautiful animated components with Motion-Primitives.
20 | {' '}
21 | Designed for developers and designers. The library leverages the
22 | power of Motion, with intuitive APIs that simplifies creating
23 | complex animations for any project. Start building more dynamic
24 | interfaces today.
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/docs/infinite-slider/infinite-slider-basic.tsx:
--------------------------------------------------------------------------------
1 | import { InfiniteSlider } from '@/components/core/infinite-slider';
2 |
3 | export function InfiniteSliderBasic() {
4 | return (
5 |
6 |
11 |
16 |
21 |
26 |
31 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/app/docs/infinite-slider/infinite-slider-hover-speed.tsx:
--------------------------------------------------------------------------------
1 | import { InfiniteSlider } from '@/components/core/infinite-slider';
2 |
3 | export function InfiniteSliderHoverSpeed() {
4 | return (
5 |
6 |
11 |
16 |
21 |
26 |
31 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/app/docs/installation/page.mdx:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | title: 'Installation - Motion-Primitives',
3 | description: 'How to install Motion-Primitives in your project.',
4 | };
5 |
6 | import CodeBlock from '@/components/website/code-block';
7 |
8 | # Installation
9 |
10 |
11 |
12 | Install Tailwind CSS
13 |
14 | Components are styled using Tailwind CSS. Follow the [official installation guide](https://tailwindcss.com/docs/installation) to add it to your project.
15 |
16 | Install Motion
17 |
18 | Components are animated using [Motion](https://motion.dev/).
19 |
20 |
21 |
22 | Add the utility helper
23 |
24 | Create a `lib/utils.ts` file to manage Tailwind CSS classes:
25 |
26 |
27 |
28 | Install icons
29 |
30 | Add [Lucide React](https://lucide.dev/) icons:
31 |
32 |
33 |
34 | That's it
35 |
36 | You can now start adding components to your project.
37 |
38 |
39 |
40 | The command above will add the Text Effect component to your project. You can then import it anywhere in your project.
41 |
42 | You can also choose manual installation and copy the code from the [components page](https://motion-primitives.com/docs/text-effect#installation).
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/docs/magnetic/magnetic-basic.tsx:
--------------------------------------------------------------------------------
1 | import { Magnetic } from '@/components/core/magnetic';
2 |
3 | export function MagneticBasic() {
4 | return (
5 |
6 |
10 | Submit
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/app/docs/magnetic/magnetic-nested.tsx:
--------------------------------------------------------------------------------
1 | import { Magnetic } from '@/components/core/magnetic';
2 |
3 | export function MagneticNested() {
4 | const springOptions = { bounce: 0.1 };
5 |
6 | return (
7 |
13 |
17 |
23 | Submit
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/docs/morphing-dialog/morphing-dialog-image.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | MorphingDialog,
3 | MorphingDialogTrigger,
4 | MorphingDialogContent,
5 | MorphingDialogClose,
6 | MorphingDialogImage,
7 | MorphingDialogContainer,
8 | } from '@/components/core/morphing-dialog';
9 | import { XIcon } from 'lucide-react';
10 |
11 | export function MorphingDialogBasicImage() {
12 | return (
13 |
19 |
20 |
25 |
26 |
27 |
28 |
33 |
34 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/docs/page.mdx:
--------------------------------------------------------------------------------
1 | # Motion-Primitives
2 |
3 | Make your website look better with beautifully designed motion components.
4 | Easy to use. Customizable. Open Source.
5 | Built for engineers, designers, and founders.
6 |
7 | Motion-Primitives offers a collection of reusable animated components built with Motion and Tailwind CSS. Add smooth, engaging animations to your web projects, making them more interactive and enjoyable.
8 |
9 | ## Vision
10 |
11 | As a developer, I struggled to find practical, high-quality animated components suitable for real-world projects. Motion-Primitives was created to fill this gap, providing ready-to-use, customizable components that improve marketing websites and web applications.
12 |
13 | ### Easy to Use and customize
14 |
15 | Each component has comprehensive examples and detailed documentation. You can use them directly or easily modify them to align with your custom UI kit, design system, or specific requirements.
16 |
17 | ### Built around Motion
18 |
19 | All components are built with [Motion](https://motion.dev/) to ensure a solid foundation and showcase the library's capabilities. Future plans include versions using CSS and Tailwind CSS only.
20 |
21 | ### Open source and community-driven
22 |
23 | Motion-Primitives is open-source. Contributions are welcome to expand and improve the collection.
24 |
--------------------------------------------------------------------------------
/app/docs/progressive-blur/progressive-blur-basic.tsx:
--------------------------------------------------------------------------------
1 | import { ProgressiveBlur } from '@/components/core/progressive-blur';
2 |
3 | export function ProgressiveBlurBasic() {
4 | return (
5 |
6 |
11 |
15 |
16 |
17 |
Benjamin Spiers
18 |
Moonlight 2023
19 |
Oil on linen. 40cm by 30cm
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/app/docs/progressive-blur/progressive-blur-hover.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { ProgressiveBlur } from '@/components/core/progressive-blur';
5 | import { motion } from 'motion/react';
6 |
7 | export function ProgressiveBlurHover() {
8 | const [isHover, setIsHover] = useState(false);
9 |
10 | return (
11 | setIsHover(true)}
14 | onMouseLeave={() => setIsHover(false)}
15 | >
16 |
21 |
31 |
40 |
41 |
John Martin
42 |
Pandemonium
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/app/docs/progressive-blur/progressive-blur-slider.tsx:
--------------------------------------------------------------------------------
1 | import { InfiniteSlider } from '@/components/core/infinite-slider';
2 | import { ProgressiveBlur } from '@/components/core/progressive-blur';
3 |
4 | export function ProgressiveBlurSlider() {
5 | return (
6 |
7 |
8 |
9 | 1
10 |
11 |
12 | 2
13 |
14 |
15 | 3
16 |
17 |
18 | 4
19 |
20 |
21 | 5
22 |
23 |
24 | 6
25 |
26 |
27 | 7
28 |
29 |
30 | 8
31 |
32 |
33 | 9
34 |
35 |
36 |
41 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/app/docs/scroll-progress/page.mdx:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | title: 'Scroll Progress - Motion-Primitives',
3 | description: 'Animated scroll progress for your web pages.',
4 | };
5 |
6 | import { ScrollProgress } from '@/components/core/scroll-progress';
7 | import { ScrollProgressBasic1 } from './scroll-progress-basic-1';
8 | import { ScrollProgressBasic2 } from './scroll-progress-basic-2';
9 | import { ScrollProgressBasic3 } from './scroll-progress-basic-3';
10 | import ComponentCodePreview from '@/components/website/component-code-preview';
11 |
12 | # Scroll Progress
13 |
14 | Animated scroll progress for your web pages.
15 |
16 | ## Examples
17 |
18 | ### Scroll Progress Basic
19 |
20 | }
22 | filePath='app/docs/scroll-progress/scroll-progress-basic-1.tsx'
23 | />
24 |
25 | ### Scroll Progress Navigation
26 |
27 | }
29 | filePath='app/docs/scroll-progress/scroll-progress-basic-2.tsx'
30 | />
31 |
32 | ### Scroll Progress Gradient
33 |
34 | }
36 | filePath='app/docs/scroll-progress/scroll-progress-basic-3.tsx'
37 | />
38 |
39 | ## Installation
40 |
41 |
42 |
43 |
44 | CLI
45 | Manual
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Copy and paste the following code into your project.
59 |
60 |
61 |
62 | Update the import paths to match your project setup.
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ## Component API
71 |
72 | ### ScrollProgress
73 |
74 | | Prop | Type | Default | Description |
75 | | :------------ | :------------ | :------ | :--------------------------------------------------------- |
76 | | className | string | | Optional CSS class for styling the scroll progress. |
77 | | springOptions | SpringOptions | | Optional spring options for the scroll progress animation. |
78 | | containerRef | RefObject | | Optional Ref object for the container element. |
79 |
80 | ## Credit
81 |
82 | Initiated by [@bhataasim1](https://github.com/bhataasim1)
83 |
--------------------------------------------------------------------------------
/app/docs/scroll-progress/scroll-progress-basic-1.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRef } from 'react';
3 | import { ScrollProgress } from '@/components/core/scroll-progress';
4 |
5 | const dummyContent = Array.from({ length: 10 }, (_, i) => (
6 |
7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam
8 | lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra
9 | nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget
10 | libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut
11 | porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies
12 | a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
13 |
14 | ));
15 |
16 | export function ScrollProgressBasic1() {
17 | const containerRef = useRef(null);
18 |
19 | return (
20 |
24 |
25 |
32 | {dummyContent}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/app/docs/scroll-progress/scroll-progress-basic-2.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRef } from 'react';
3 | import { ScrollProgress } from '@/components/core/scroll-progress';
4 |
5 | const dummyContent = Array.from({ length: 10 }, (_, i) => (
6 |
7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam
8 | lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra
9 | nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget
10 | libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut
11 | porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies
12 | a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
13 |
14 | ));
15 |
16 | export function ScrollProgressBasic2() {
17 | const containerRef = useRef(null);
18 |
19 | return (
20 |
21 |
47 | {dummyContent}
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/docs/scroll-progress/scroll-progress-basic-3.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRef } from 'react';
3 | import { ScrollProgress } from '@/components/core/scroll-progress';
4 |
5 | const dummyContent = Array.from({ length: 10 }, (_, i) => (
6 |
7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam
8 | lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra
9 | nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget
10 | libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut
11 | porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies
12 | a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
13 |
14 | ));
15 |
16 | export function ScrollProgressBasic3() {
17 | const containerRef = useRef(null);
18 |
19 | return (
20 |
24 |
25 |
37 | {dummyContent}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/docs/sliding-number/clock.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { SlidingNumber } from '@/components/core/sliding-number';
3 | import { useEffect, useState } from 'react';
4 |
5 | export function Clock() {
6 | const [hours, setHours] = useState(new Date().getHours());
7 | const [minutes, setMinutes] = useState(new Date().getMinutes());
8 | const [seconds, setSeconds] = useState(new Date().getSeconds());
9 |
10 | useEffect(() => {
11 | const interval = setInterval(() => {
12 | setHours(new Date().getHours());
13 | setMinutes(new Date().getMinutes());
14 | setSeconds(new Date().getSeconds());
15 | }, 1000);
16 | return () => clearInterval(interval);
17 | }, []);
18 |
19 | return (
20 |
21 |
22 | :
23 |
24 | :
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/docs/sliding-number/page.mdx:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | title: 'Sliding Number - Motion-Primitives',
3 | description: 'A component that slides numbers.',
4 | };
5 |
6 | import { Clock } from './clock';
7 | import { SlidingNumberBasic } from './sliding-basic';
8 | import { SlidingNumberWithSlider } from './sliding-slider';
9 | import ComponentCodePreview from '@/components/website/component-code-preview';
10 |
11 | # Sliding Number
12 |
13 | A component that slides numbers.
14 |
15 | ## Examples
16 |
17 | ### Clock
18 |
19 | }
21 | filePath='app/docs/sliding-number/clock.tsx'
22 | />
23 |
24 | ### Sliding number with slider
25 |
26 | }
28 | filePath='app/docs/sliding-number/sliding-slider.tsx'
29 | />
30 |
31 | ### Sliding number basic
32 |
33 | }
35 | filePath='app/docs/sliding-number/sliding-basic.tsx'
36 | hasReTrigger
37 | />
38 |
39 | ## Installation
40 |
41 |
42 |
43 |
44 | CLI
45 | Manual
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Copy and paste the following code into your project.
59 |
60 |
61 |
62 | Update the import paths to match your project setup.
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ## Component API
71 |
72 | ### SlidingNumber
73 |
74 | | Prop | Type | Default | Description |
75 | | :--------------- | :------ | :------ | :------------------------------------------------------------------------- |
76 | | value | string | | The numerical value to be displayed. |
77 | | padStart | boolean | false | Whether to pad the integer part with a leading zero if it is less than 10. |
78 | | decimalSeparator | string | '.' | The character to use as the decimal separator. |
79 |
80 | ## Credits
81 |
82 | Inspired by [Number Flow](https://number-flow.barvian.me/) and [Animated Counter](https://buildui.com/recipes/animated-counter).
83 |
--------------------------------------------------------------------------------
/app/docs/sliding-number/sliding-basic.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion } from 'motion/react';
4 | import { useEffect, useState } from 'react';
5 | import { SlidingNumber } from '@/components/core/sliding-number';
6 |
7 | export function SlidingNumberBasic() {
8 | const [value, setValue] = useState(0);
9 |
10 | useEffect(() => {
11 | if (value === 100) return;
12 |
13 | const interval = setInterval(() => {
14 | setValue(value + 1);
15 | }, 10);
16 | return () => clearInterval(interval);
17 | }, [value]);
18 |
19 | return (
20 |
30 |
31 | %
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/app/docs/sliding-number/sliding-slider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { SlidingNumber } from '@/components/core/sliding-number';
3 | import { useState } from 'react';
4 |
5 | export function SlidingNumberWithSlider() {
6 | const [value, setValue] = useState(100);
7 | const [width, setWidth] = useState(0);
8 |
9 | return (
10 |
11 |
Current ARR:
12 |
13 | $
14 |
15 |
setValue(+e.target.value)}
22 | className='mt-2 accent-indigo-950'
23 | />
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/docs/spinning-text/spinning-text-basic.tsx:
--------------------------------------------------------------------------------
1 | import { SpinningText } from '@/components/core/spinning-text';
2 |
3 | export function SpinningTextBasic() {
4 | return (
5 |
10 | {`pre-order • pre-order • pre-order • `}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/docs/spinning-text/spinning-text-custom-transition.tsx:
--------------------------------------------------------------------------------
1 | import { SpinningText } from '@/components/core/spinning-text';
2 |
3 | export function SpinningTextCustomTransition() {
4 | return (
5 |
15 | {`motion-primitives • motion-primitives • `}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/docs/spinning-text/spinning-text-custom-variants.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SpinningText } from '@/components/core/spinning-text';
3 |
4 | export function SpinningTextCustomVariants() {
5 | return (
6 |
39 | {`pre-order • pre-order • pre-order • `}
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/app/docs/spotlight/spotlight-basic.tsx:
--------------------------------------------------------------------------------
1 | import { Spotlight } from '@/components/core/spotlight';
2 |
3 | export function SpotlightBasic() {
4 | return (
5 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/app/docs/spotlight/spotlight-border.tsx:
--------------------------------------------------------------------------------
1 | import { Spotlight } from '@/components/core/spotlight';
2 |
3 | export function SpotlightBorder() {
4 | return (
5 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/docs/spotlight/spotlight-custom-color.tsx:
--------------------------------------------------------------------------------
1 | import { Spotlight } from '@/components/core/spotlight';
2 |
3 | export function SpotlightCustomColor() {
4 | return (
5 |
6 |
10 |
11 |
12 |
13 |
19 |
26 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/app/docs/text-effect/text-effect-custom-delay.tsx:
--------------------------------------------------------------------------------
1 | import { TextEffect } from '@/components/core/text-effect';
2 |
3 | export function TextEffectWithCustomDelay() {
4 | return (
5 |
6 |
38 | Animate your ideas
39 |
40 |
41 | with motion-primitives
42 |
43 |
49 | (and delay!)
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/app/docs/text-effect/text-effect-exit.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState, useEffect } from 'react';
3 | import { TextEffect } from '@/components/core/text-effect';
4 |
5 | export function TextEffectWithExit() {
6 | const [trigger, setTrigger] = useState(true);
7 |
8 | useEffect(() => {
9 | const interval = setInterval(() => {
10 | setTrigger((prev) => !prev);
11 | }, 2000);
12 |
13 | return () => clearInterval(interval);
14 | }, []);
15 | const blurSlideVariants = {
16 | container: {
17 | hidden: { opacity: 0 },
18 | visible: {
19 | opacity: 1,
20 | transition: { staggerChildren: 0.01 },
21 | },
22 | exit: {
23 | transition: { staggerChildren: 0.01, staggerDirection: 1 },
24 | },
25 | },
26 | item: {
27 | hidden: {
28 | opacity: 0,
29 | filter: 'blur(10px) brightness(0%)',
30 | y: 0,
31 | },
32 | visible: {
33 | opacity: 1,
34 | y: 0,
35 | filter: 'blur(0px) brightness(100%)',
36 | transition: {
37 | duration: 0.4,
38 | },
39 | },
40 | exit: {
41 | opacity: 0,
42 | y: -30,
43 | filter: 'blur(10px) brightness(0%)',
44 | transition: {
45 | duration: 0.4,
46 | },
47 | },
48 | },
49 | };
50 |
51 | return (
52 |
58 | Animate your ideas with motion-primitives
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/app/docs/text-effect/text-effect-line.tsx:
--------------------------------------------------------------------------------
1 | import { TextEffect } from '@/components/core/text-effect';
2 |
3 | export function TextEffectPerLine() {
4 | return (
5 |
32 | {`now live on motion-primitives!
33 | now live on motion-primitives!
34 | now live on motion-primitives!`}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/app/docs/text-effect/text-effect-per-char.tsx:
--------------------------------------------------------------------------------
1 | import { TextEffect } from '@/components/core/text-effect';
2 |
3 | export function TextEffectPerChar() {
4 | return (
5 |
6 | Animate your ideas with motion-primitives
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/docs/text-effect/text-effect-per-word.tsx:
--------------------------------------------------------------------------------
1 | import { TextEffect } from '../../../components/core/text-effect';
2 |
3 | export function TextEffectPerWord() {
4 | return (
5 |
6 | Animate your ideas with motion-primitives
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/docs/text-effect/text-effect-preset.tsx:
--------------------------------------------------------------------------------
1 | import { TextEffect } from '@/components/core/text-effect';
2 |
3 | export function TextEffectWithPreset() {
4 | return (
5 |
6 | Animate your ideas with motion-primitives
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/docs/text-effect/text-effect-speed.tsx:
--------------------------------------------------------------------------------
1 | import { TextEffect } from '@/components/core/text-effect';
2 |
3 | export function TextEffectSpeed() {
4 | return (
5 |
6 | Animate your ideas with motion-primitives.
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/docs/text-effect/text-effect-variants.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { TextEffect } from '@/components/core/text-effect';
3 |
4 | export function TextEffectWithCustomVariants() {
5 | const getRandomColor = () => {
6 | const letters = '0123456789ABCDEF';
7 | let color = '#';
8 | for (let i = 0; i < 6; i++) {
9 | color += letters[Math.floor(Math.random() * 16)];
10 | }
11 | return color;
12 | };
13 |
14 | const fancyVariants = {
15 | container: {
16 | hidden: { opacity: 0 },
17 | visible: {
18 | opacity: 1,
19 | transition: {
20 | staggerChildren: 0.05,
21 | },
22 | },
23 | },
24 | item: {
25 | hidden: () => ({
26 | opacity: 0,
27 | y: Math.random() * 100 - 50,
28 | rotate: Math.random() * 90 - 45,
29 | scale: 0.3,
30 | color: getRandomColor(),
31 | }),
32 | visible: {
33 | opacity: 1,
34 | y: 0,
35 | rotate: 0,
36 | scale: 1,
37 | color: getRandomColor(),
38 | transition: {
39 | type: 'spring',
40 | damping: 12,
41 | stiffness: 200,
42 | },
43 | },
44 | },
45 | };
46 |
47 | return (
48 |
49 | Animate your ideas with motion-primitives
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/app/docs/text-loop/text-loop-basic.tsx:
--------------------------------------------------------------------------------
1 | import { TextLoop } from '@/components/core/text-loop';
2 |
3 | export function TextLoopBasic() {
4 | return (
5 |
6 | How can I assist you today?
7 | Generate a logo
8 | Create a component
9 | Draw a diagram
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/app/docs/text-loop/text-loop-custom-variants-transition.tsx:
--------------------------------------------------------------------------------
1 | import { TextLoop } from '@/components/core/text-loop';
2 |
3 | export function TextLoopCustomVariantsTransition() {
4 | return (
5 |
6 | Beautiful templates for{' '}
7 |
36 | Founders
37 | Developers
38 | Designers
39 | Design Engineers
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/app/docs/text-loop/text-loop-on-index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { TextLoop } from '@/components/core/text-loop';
3 | import { Music } from 'lucide-react';
4 | import { useState } from 'react';
5 |
6 | export function TextLoopOnIndexChange() {
7 | const [direction, setDirection] = useState(-1);
8 |
9 | return (
10 | {
20 | setDirection(index === 0 ? -1 : 1);
21 | }}
22 | variants={{
23 | initial: {
24 | y: -direction * 20,
25 | rotateX: -direction * 90,
26 | opacity: 0,
27 | filter: 'blur(4px)',
28 | },
29 | animate: {
30 | y: 0,
31 | rotateX: 0,
32 | opacity: 1,
33 | filter: 'blur(0px)',
34 | },
35 | exit: {
36 | y: -direction * 20,
37 | rotateX: -direction * 90,
38 | opacity: 0,
39 | filter: 'blur(4px)',
40 | },
41 | }}
42 | >
43 |
44 | A Little Lost・Arthur
45 | Russell
46 |
47 | La Trinité, Martinique
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/docs/text-morph/page.mdx:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | title: 'Text Morph - Motion-Primitives',
3 | description:
4 | 'Animates text by morphing shared letters between words, creating fluid transitions.',
5 | };
6 |
7 | import { TextMorphButton } from './text-morph-button';
8 | import { TextMorphInput } from './text-morph-input';
9 | import ComponentCodePreview from '@/components/website/component-code-preview';
10 | import CodeBlock from '@/components/website/code-block';
11 |
12 | # Text Morph
13 |
14 | Animates text by morphing shared letters between words, creating fluid transitions
15 |
16 | ## Examples
17 |
18 | ### Text Morph Button
19 |
20 | }
22 | filePath='app/docs/text-morph/text-morph-button.tsx'
23 | />
24 |
25 | ### Text Morph Input
26 |
27 | }
29 | filePath='app/docs/text-morph/text-morph-input.tsx'
30 | classNameComponentContainer='min-h-[300px] py-24 lg:px-32 px-8 items-end'
31 | />
32 |
33 | ## Installation
34 |
35 |
36 |
37 |
38 | CLI
39 | Manual
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Copy and paste the following code into your project.
53 |
54 |
55 |
56 | Update the import paths to match your project setup.
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | ## Component API
65 |
66 | ### TextMorph
67 |
68 | | Prop | Type | Default | Description |
69 | | :-------- | :-------------------------- | :-------- | :--------------------------------------------- |
70 | | children | string | | The text content to be animated. |
71 | | as | keyof JSX.IntrinsicElements | 'p' | The HTML tag to render, defaults to paragraph. |
72 | | className | string | undefined | Optional CSS class for styling the component. |
73 | | style | React.CSSProperties | undefined | Optional inline styles for the component. |
74 |
75 | ## Credits
76 |
77 | Inspired by [Family](https://family.co/) iOS app. See also [Family Values](https://benji.org/family-values#fluidity-through-seamless-transitions)
78 |
--------------------------------------------------------------------------------
/app/docs/text-morph/text-morph-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState } from 'react';
3 | import { TextMorph } from '@/components/core/text-morph';
4 |
5 | export function TextMorphButton() {
6 | const [text, setText] = useState('Continue');
7 |
8 | return (
9 | setText(text === 'Continue' ? 'Confirm' : 'Continue')}
11 | className='flex h-10 w-[120px] shrink-0 items-center justify-center rounded-full bg-black px-4 text-base font-medium text-zinc-50 shadow-xs transition-colors hover:bg-zinc-800 dark:bg-zinc-50 dark:text-black dark:hover:bg-zinc-200'
12 | >
13 | {text}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/app/docs/text-morph/text-morph-input.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState } from 'react';
3 | import { TextMorph } from '../../../components/core/text-morph';
4 |
5 | export function TextMorphInput() {
6 | const [text, setText] = useState('Craft');
7 |
8 | return (
9 |
10 |
11 | {text}
12 |
13 | setText(e.target.value)}
17 | className='h-9 w-full rounded-lg border border-zinc-950/10 bg-transparent p-2 text-zinc-900 placeholder-zinc-500 focus:outline-hidden dark:border-zinc-50/10 dark:text-white dark:placeholder-zinc-400'
18 | placeholder='Type your text here'
19 | />
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/app/docs/text-roll/text-roll-basic.tsx:
--------------------------------------------------------------------------------
1 | import { TextRoll } from '@/components/core/text-roll';
2 |
3 | export function TextRollBasic() {
4 | return (
5 |
6 | motion-primitives
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/docs/text-roll/text-roll-custom-transition-delay.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { TextRoll } from '@/components/core/text-roll';
3 |
4 | export function TextRollCustomTransitionDelay() {
5 | return (
6 | i * 0.05}
20 | getExitDelay={(i) => i * 0.05 + 0.05}
21 | transition={{
22 | ease: [0.175, 0.885, 0.32, 1.1],
23 | }}
24 | >
25 | motion-primitives
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/docs/text-roll/text-roll-custom-variants.tsx:
--------------------------------------------------------------------------------
1 | import { TextRoll } from '@/components/core/text-roll';
2 |
3 | export function TextRollCustomVariants() {
4 | return (
5 |
18 | motion-primitives
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/app/docs/text-scramble/text-scramble-basic.tsx:
--------------------------------------------------------------------------------
1 | import { TextScramble } from '@/components/core/text-scramble';
2 |
3 | export function TextScrambleBasic() {
4 | return (
5 |
6 | Text Scramble
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/docs/text-scramble/text-scramble-custom-char-duration.tsx:
--------------------------------------------------------------------------------
1 | import { TextScramble } from '@/components/core/text-scramble';
2 |
3 | export function TextScrambleCustomCharacterDuration() {
4 | return (
5 |
10 | Generating the interface...
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/docs/text-scramble/text-scramble-custom-trigger.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { TextScramble } from '@/components/core/text-scramble';
3 | import { useState } from 'react';
4 |
5 | export function TextScrambleCustomTrigger() {
6 | const [isTrigger, setIsTrigger] = useState(false);
7 |
8 | return (
9 |
13 | setIsTrigger(true)}
19 | onScrambleComplete={() => setIsTrigger(false)}
20 | >
21 | Tyler, The Creator - I Hope You Fin Your Way Home
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/app/docs/text-shimmer-wave/text-shimmer-wave-basic.tsx:
--------------------------------------------------------------------------------
1 | import { TextShimmerWave } from '@/components/core/text-shimmer-wave';
2 |
3 | export function TextShimmerWaveBasic() {
4 | return (
5 |
6 | Generating code...
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/docs/text-shimmer-wave/text-shimmer-wave-color.tsx:
--------------------------------------------------------------------------------
1 | import { TextShimmerWave } from '@/components/core/text-shimmer-wave';
2 |
3 | export function TextShimmerWaveColor() {
4 | return (
5 |
13 | Creating the perfect dish...
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/app/docs/text-shimmer/text-shimmer-basic.tsx:
--------------------------------------------------------------------------------
1 | import { TextShimmer } from '@/components/core/text-shimmer';
2 |
3 | export function TextShimmerBasic() {
4 | return (
5 |
6 | Generating code...
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/app/docs/text-shimmer/text-shimmer-color.tsx:
--------------------------------------------------------------------------------
1 | import { TextShimmer } from '@/components/core/text-shimmer';
2 |
3 | export function TextShimmerColor() {
4 | return (
5 |
9 | Hi, how are you?
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/app/docs/tilt/tilt-card-1.tsx:
--------------------------------------------------------------------------------
1 | import { Tilt } from '@/components/core/tilt';
2 |
3 | export function TiltCard1() {
4 | return (
5 |
6 |
12 |
17 |
18 |
19 | Ghost in the Shell
20 |
21 |
Kôkaku kidôtai
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/docs/tilt/tilt-spotlight.tsx:
--------------------------------------------------------------------------------
1 | import { Spotlight } from '@/components/core/spotlight';
2 | import { Tilt } from '@/components/core/tilt';
3 |
4 | export function TiltSpotlight() {
5 | return (
6 |
7 |
20 |
29 |
35 |
36 |
37 |
38 | 2001: A Space Odyssey
39 |
40 |
Stanley Kubrick
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/app/docs/toolbar-dynamic/page.mdx:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | title: 'Toolbar Dynamic - Motion-Primitives',
3 | description:
4 | 'Adjusts width dynamically to accommodate varying tool requirements. Starts small and expands to offer more tools as needed.',
5 | };
6 |
7 | import ToolbarDynamic from '@/components/core/toolbar-dynamic';
8 | import ComponentCodePreview from '@/components/website/component-code-preview';
9 |
10 | # Toolbar Dynamic
11 |
12 | A toolbar that adjusts its width based on what you need to do. It starts small and can expand to offer more tools.
13 |
14 | }
16 | filePath='components/core/toolbar-dynamic.tsx'
17 | />
18 |
19 | **Please add:**
20 |
21 | - [useClickOutside](https://github.com/ibelick/motion-primitives/blob/main/hooks/useClickOutside.tsx)
22 |
--------------------------------------------------------------------------------
/app/docs/toolbar-expandable/page.mdx:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | title: 'Toolbar Expandable - Motion-Primitives',
3 | description:
4 | 'Changes height based on content, expanding to display more tools and shrinking to save space. Keeps your screen organized and efficient.',
5 | };
6 |
7 | import ToolbarExpandable from '@/components/core/toolbar-expandable';
8 | import ComponentCodePreview from '@/components/website/component-code-preview';
9 |
10 | # Toolbar Dynamic
11 |
12 | A toolbar that changes its height as needed. It grows to show more content and shrinks to save space, keeping your screen neat.
13 |
14 | }
16 | filePath='components/core/toolbar-expandable.tsx'
17 | />
18 |
19 | **Please add:**
20 |
21 | - [useClickOutside](https://github.com/ibelick/motion-primitives/blob/main/hooks/useClickOutside.tsx)
22 | - [cn](https://github.com/ibelick/motion-primitives/blob/main/lib/utils.ts)
23 |
--------------------------------------------------------------------------------
/app/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibelick/motion-primitives/ab4563f012c063e542de3babe3b901a8b521a7de/app/icon.ico
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 | import type { Metadata } from 'next';
3 | import { Inter } from 'next/font/google';
4 | import Script from 'next/script';
5 | import { ThemeProvider } from '@/components/website/theme-provider';
6 | import { GeistMono } from 'geist/font/mono';
7 | import { TooltipProvider } from '@/components/ui/tooltip';
8 | const inter = Inter({ subsets: ['latin'] });
9 | const geistMono = GeistMono;
10 |
11 | export const metadata: Metadata = {
12 | title: 'Motion-Primitives',
13 | description:
14 | 'UI kit to make beautiful, animated interfaces, faster. Open-source and customizable.',
15 | };
16 |
17 | export default function RootLayout({
18 | children,
19 | }: {
20 | children: React.ReactNode;
21 | }) {
22 | const isDev = process.env.NODE_ENV === 'development';
23 |
24 | return (
25 |
26 | {!isDev ? (
27 |
32 | ) : null}
33 |
36 |
37 |
38 | {children}
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/app/opengraph-image.alt:
--------------------------------------------------------------------------------
1 | UI kit to make beautiful, animated interfaces, faster. Open-source and customizable.
2 |
--------------------------------------------------------------------------------
/app/opengraph-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibelick/motion-primitives/ab4563f012c063e542de3babe3b901a8b521a7de/app/opengraph-image.jpg
--------------------------------------------------------------------------------
/app/twitter-image.alt:
--------------------------------------------------------------------------------
1 | UI kit to make beautiful, animated interfaces, faster. Open-source and customizable.
2 |
--------------------------------------------------------------------------------
/app/twitter-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibelick/motion-primitives/ab4563f012c063e542de3babe3b901a8b521a7de/app/twitter-image.jpg
--------------------------------------------------------------------------------
/cli/README.md:
--------------------------------------------------------------------------------
1 | # Motion Primitives CLI
2 |
3 | A command-line interface for easily adding beautiful, animated components to your React project.
4 |
5 | ## Installation
6 |
7 | You can use the CLI directly with npx:
8 |
9 | ```bash
10 | npx motion-primitives
11 | ```
12 |
13 | Or install it globally:
14 |
15 | ```bash
16 | npm install -g motion-primitives
17 | motion-primitives
18 | ```
19 |
20 | ## Usage
21 |
22 | ### List available components
23 |
24 | To see all available animated components:
25 |
26 | ```bash
27 | npx motion-primitives list
28 | ```
29 |
30 | This will display a list of all available components along with descriptions and required dependencies.
31 |
32 | ### Add a component
33 |
34 | To add a specific component to your project:
35 |
36 | ```bash
37 | npx motion-primitives add
38 | ```
39 |
40 | For example:
41 |
42 | ```bash
43 | npx motion-primitives add text-morph
44 | ```
45 |
46 | This will:
47 |
48 | 1. Create a `components/motion-primitives` directory in your project (if it doesn't exist)
49 | 2. Download and add the component files
50 | 3. Automatically install any required dependencies using your preferred package manager (npm, yarn, or pnpm)
51 |
52 | The CLI automatically detects which package manager you're using based on lock files in your project.
53 |
54 | ## Available Components
55 |
56 | Motion Primitives offers a variety of beautiful and performant animated components, including:
57 |
58 | - **Animated UI Elements**: Accordion, Dialog, Text Effects, Carousels
59 | - **Interactive Animations**: Magnetic elements, Spotlights, Tilt effects
60 | - **Text Animations**: Text morphing, Text loops, Text scramble effects, Spinning text
61 | - **And many more!**
62 |
63 | For the complete list, run `npx motion-primitives list`.
64 |
65 | ## Dependencies
66 |
67 | Most components require the `motion` library. When you add a component, the CLI will automatically install the required dependencies using your preferred package manager.
68 |
69 | ## Contributing
70 |
71 | Contributions are welcome! Please feel free to submit a Pull Request.
72 |
73 | ## License
74 |
75 | [MIT](LICENSE)
76 |
--------------------------------------------------------------------------------
/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "motion-primitives",
3 | "version": "0.1.0",
4 | "description": "CLI to add beautiful, animated components to your app",
5 | "bin": {
6 | "motion-primitives": "./dist/index.js"
7 | },
8 | "scripts": {
9 | "build": "tsc",
10 | "start": "node dist/index.js",
11 | "dev": "tsc -w",
12 | "prepublishOnly": "npm run build"
13 | },
14 | "dependencies": {
15 | "commander": "^9.4.1",
16 | "ora": "^5.4.1",
17 | "node-fetch": "^2.6.7"
18 | },
19 | "devDependencies": {
20 | "typescript": "^4.9.5",
21 | "@types/node": "^18.0.0",
22 | "@types/node-fetch": "^2.6.2"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/ibelick/motion-primitives.git",
27 | "directory": "cli"
28 | },
29 | "keywords": [
30 | "animation",
31 | "components",
32 | "motion",
33 | "ui",
34 | "react",
35 | "cli"
36 | ],
37 | "author": "ibelick",
38 | "license": "MIT"
39 | }
40 |
--------------------------------------------------------------------------------
/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "CommonJS",
5 | "outDir": "./dist",
6 | "rootDir": "./src",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true
10 | },
11 | "include": ["src/**/*"],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/components/core/animated-number.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import { motion, SpringOptions, useSpring, useTransform } from 'motion/react';
4 | import { useEffect } from 'react';
5 |
6 | export type AnimatedNumberProps = {
7 | value: number;
8 | className?: string;
9 | springOptions?: SpringOptions;
10 | as?: React.ElementType;
11 | };
12 |
13 | export function AnimatedNumber({
14 | value,
15 | className,
16 | springOptions,
17 | as = 'span',
18 | }: AnimatedNumberProps) {
19 | const MotionComponent = motion.create(as as keyof JSX.IntrinsicElements);
20 |
21 | const spring = useSpring(value, springOptions);
22 | const display = useTransform(spring, (current) =>
23 | Math.round(current).toLocaleString()
24 | );
25 |
26 | useEffect(() => {
27 | spring.set(value);
28 | }, [spring, value]);
29 |
30 | return (
31 |
32 | {display}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/core/border-trail.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import { motion, Transition } from 'motion/react';
4 |
5 | export type BorderTrailProps = {
6 | className?: string;
7 | size?: number;
8 | transition?: Transition;
9 | onAnimationComplete?: () => void;
10 | style?: React.CSSProperties;
11 | };
12 |
13 | export function BorderTrail({
14 | className,
15 | size = 60,
16 | transition,
17 | onAnimationComplete,
18 | style,
19 | }: BorderTrailProps) {
20 | const defaultTransition: Transition = {
21 | repeat: Infinity,
22 | duration: 5,
23 | ease: 'linear',
24 | };
25 |
26 | return (
27 |
28 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/core/in-view.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { ReactNode, useRef } from 'react';
3 | import {
4 | motion,
5 | useInView,
6 | Variant,
7 | Transition,
8 | UseInViewOptions,
9 | } from 'motion/react';
10 |
11 | export type InViewProps = {
12 | children: ReactNode;
13 | variants?: {
14 | hidden: Variant;
15 | visible: Variant;
16 | };
17 | transition?: Transition;
18 | viewOptions?: UseInViewOptions;
19 | as?: React.ElementType;
20 | };
21 |
22 | const defaultVariants = {
23 | hidden: { opacity: 0 },
24 | visible: { opacity: 1 },
25 | };
26 |
27 | export function InView({
28 | children,
29 | variants = defaultVariants,
30 | transition,
31 | viewOptions,
32 | as = 'div',
33 | }: InViewProps) {
34 | const ref = useRef(null);
35 | const isInView = useInView(ref, viewOptions);
36 |
37 | const MotionComponent = motion[as as keyof typeof motion] as typeof as;
38 |
39 | return (
40 |
47 | {children}
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/core/progressive-blur.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import { HTMLMotionProps, motion } from 'motion/react';
4 |
5 | export const GRADIENT_ANGLES = {
6 | top: 0,
7 | right: 90,
8 | bottom: 180,
9 | left: 270,
10 | };
11 |
12 | export type ProgressiveBlurProps = {
13 | direction?: keyof typeof GRADIENT_ANGLES;
14 | blurLayers?: number;
15 | className?: string;
16 | blurIntensity?: number;
17 | } & HTMLMotionProps<'div'>;
18 |
19 | export function ProgressiveBlur({
20 | direction = 'bottom',
21 | blurLayers = 8,
22 | className,
23 | blurIntensity = 0.25,
24 | ...props
25 | }: ProgressiveBlurProps) {
26 | const layers = Math.max(blurLayers, 2);
27 | const segmentSize = 1 / (blurLayers + 1);
28 |
29 | return (
30 |
31 | {Array.from({ length: layers }).map((_, index) => {
32 | const angle = GRADIENT_ANGLES[direction];
33 | const gradientStops = [
34 | index * segmentSize,
35 | (index + 1) * segmentSize,
36 | (index + 2) * segmentSize,
37 | (index + 3) * segmentSize,
38 | ].map(
39 | (pos, posIndex) =>
40 | `rgba(255, 255, 255, ${posIndex === 1 || posIndex === 2 ? 1 : 0}) ${pos * 100}%`
41 | );
42 |
43 | const gradient = `linear-gradient(${angle}deg, ${gradientStops.join(
44 | ', '
45 | )})`;
46 |
47 | return (
48 |
58 | );
59 | })}
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/components/core/scroll-progress.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion, SpringOptions, useScroll, useSpring } from 'motion/react';
4 | import { cn } from '@/lib/utils';
5 | import { RefObject } from 'react';
6 |
7 | export type ScrollProgressProps = {
8 | className?: string;
9 | springOptions?: SpringOptions;
10 | containerRef?: RefObject;
11 | };
12 |
13 | const DEFAULT_SPRING_OPTIONS: SpringOptions = {
14 | stiffness: 200,
15 | damping: 50,
16 | restDelta: 0.001,
17 | };
18 |
19 | export function ScrollProgress({
20 | className,
21 | springOptions,
22 | containerRef,
23 | }: ScrollProgressProps) {
24 | const { scrollYProgress } = useScroll({
25 | container: containerRef,
26 | layoutEffect: Boolean(containerRef?.current),
27 | });
28 |
29 | const scaleX = useSpring(scrollYProgress, {
30 | ...DEFAULT_SPRING_OPTIONS,
31 | ...(springOptions ?? {}),
32 | });
33 |
34 | return (
35 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/components/core/text-loop.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import {
4 | motion,
5 | AnimatePresence,
6 | Transition,
7 | Variants,
8 | AnimatePresenceProps,
9 | } from 'motion/react';
10 | import { useState, useEffect, Children } from 'react';
11 |
12 | export type TextLoopProps = {
13 | children: React.ReactNode[];
14 | className?: string;
15 | interval?: number;
16 | transition?: Transition;
17 | variants?: Variants;
18 | onIndexChange?: (index: number) => void;
19 | trigger?: boolean;
20 | mode?: AnimatePresenceProps['mode'];
21 | };
22 |
23 | export function TextLoop({
24 | children,
25 | className,
26 | interval = 2,
27 | transition = { duration: 0.3 },
28 | variants,
29 | onIndexChange,
30 | trigger = true,
31 | mode = 'popLayout',
32 | }: TextLoopProps) {
33 | const [currentIndex, setCurrentIndex] = useState(0);
34 | const items = Children.toArray(children);
35 |
36 | useEffect(() => {
37 | if (!trigger) return;
38 |
39 | const intervalMs = interval * 1000;
40 | const timer = setInterval(() => {
41 | setCurrentIndex((current) => {
42 | const next = (current + 1) % items.length;
43 | onIndexChange?.(next);
44 | return next;
45 | });
46 | }, intervalMs);
47 | return () => clearInterval(timer);
48 | }, [items.length, interval, onIndexChange, trigger]);
49 |
50 | const motionVariants: Variants = {
51 | initial: { y: 20, opacity: 0 },
52 | animate: { y: 0, opacity: 1 },
53 | exit: { y: -20, opacity: 0 },
54 | };
55 |
56 | return (
57 |
58 |
59 |
67 | {items[currentIndex]}
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/components/core/text-morph.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import { AnimatePresence, motion, Transition, Variants } from 'motion/react';
4 | import { useMemo, useId } from 'react';
5 |
6 | export type TextMorphProps = {
7 | children: string;
8 | as?: React.ElementType;
9 | className?: string;
10 | style?: React.CSSProperties;
11 | variants?: Variants;
12 | transition?: Transition;
13 | };
14 |
15 | export function TextMorph({
16 | children,
17 | as: Component = 'p',
18 | className,
19 | style,
20 | variants,
21 | transition,
22 | }: TextMorphProps) {
23 | const uniqueId = useId();
24 |
25 | const characters = useMemo(() => {
26 | const charCounts: Record = {};
27 |
28 | return children.split('').map((char) => {
29 | const lowerChar = char.toLowerCase();
30 | charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;
31 |
32 | return {
33 | id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
34 | label: char === ' ' ? '\u00A0' : char,
35 | };
36 | });
37 | }, [children, uniqueId]);
38 |
39 | const defaultVariants: Variants = {
40 | initial: { opacity: 0 },
41 | animate: { opacity: 1 },
42 | exit: { opacity: 0 },
43 | };
44 |
45 | const defaultTransition: Transition = {
46 | type: 'spring',
47 | stiffness: 280,
48 | damping: 18,
49 | mass: 0.3,
50 | };
51 |
52 | return (
53 |
54 |
55 | {characters.map((character) => (
56 |
67 | {character.label}
68 |
69 | ))}
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/components/core/text-scramble.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { type JSX, useEffect, useState } from 'react';
3 | import { motion, MotionProps } from 'motion/react';
4 |
5 | export type TextScrambleProps = {
6 | children: string;
7 | duration?: number;
8 | speed?: number;
9 | characterSet?: string;
10 | as?: React.ElementType;
11 | className?: string;
12 | trigger?: boolean;
13 | onScrambleComplete?: () => void;
14 | } & MotionProps;
15 |
16 | const defaultChars =
17 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
18 |
19 | export function TextScramble({
20 | children,
21 | duration = 0.8,
22 | speed = 0.04,
23 | characterSet = defaultChars,
24 | className,
25 | as: Component = 'p',
26 | trigger = true,
27 | onScrambleComplete,
28 | ...props
29 | }: TextScrambleProps) {
30 | const MotionComponent = motion.create(
31 | Component as keyof JSX.IntrinsicElements
32 | );
33 | const [displayText, setDisplayText] = useState(children);
34 | const [isAnimating, setIsAnimating] = useState(false);
35 | const text = children;
36 |
37 | const scramble = async () => {
38 | if (isAnimating) return;
39 | setIsAnimating(true);
40 |
41 | const steps = duration / speed;
42 | let step = 0;
43 |
44 | const interval = setInterval(() => {
45 | let scrambled = '';
46 | const progress = step / steps;
47 |
48 | for (let i = 0; i < text.length; i++) {
49 | if (text[i] === ' ') {
50 | scrambled += ' ';
51 | continue;
52 | }
53 |
54 | if (progress * text.length > i) {
55 | scrambled += text[i];
56 | } else {
57 | scrambled +=
58 | characterSet[Math.floor(Math.random() * characterSet.length)];
59 | }
60 | }
61 |
62 | setDisplayText(scrambled);
63 | step++;
64 |
65 | if (step > steps) {
66 | clearInterval(interval);
67 | setDisplayText(text);
68 | setIsAnimating(false);
69 | onScrambleComplete?.();
70 | }
71 | }, speed * 1000);
72 | };
73 |
74 | useEffect(() => {
75 | if (!trigger) return;
76 |
77 | scramble();
78 | }, [trigger]);
79 |
80 | return (
81 |
82 | {displayText}
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/components/core/text-shimmer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useMemo, type JSX } from 'react';
3 | import { motion } from 'motion/react';
4 | import { cn } from '@/lib/utils';
5 |
6 | export type TextShimmerProps = {
7 | children: string;
8 | as?: React.ElementType;
9 | className?: string;
10 | duration?: number;
11 | spread?: number;
12 | };
13 |
14 | function TextShimmerComponent({
15 | children,
16 | as: Component = 'p',
17 | className,
18 | duration = 2,
19 | spread = 2,
20 | }: TextShimmerProps) {
21 | const MotionComponent = motion.create(
22 | Component as keyof JSX.IntrinsicElements
23 | );
24 |
25 | const dynamicSpread = useMemo(() => {
26 | return children.length * spread;
27 | }, [children, spread]);
28 |
29 | return (
30 |
52 | {children}
53 |
54 | );
55 | }
56 |
57 | export const TextShimmer = React.memo(TextShimmerComponent);
58 |
--------------------------------------------------------------------------------
/components/core/tilt.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useRef } from 'react';
4 | import {
5 | motion,
6 | useMotionTemplate,
7 | useMotionValue,
8 | useSpring,
9 | useTransform,
10 | MotionStyle,
11 | SpringOptions,
12 | } from 'motion/react';
13 |
14 | export type TiltProps = {
15 | children: React.ReactNode;
16 | className?: string;
17 | style?: MotionStyle;
18 | rotationFactor?: number;
19 | isRevese?: boolean;
20 | springOptions?: SpringOptions;
21 | };
22 |
23 | export function Tilt({
24 | children,
25 | className,
26 | style,
27 | rotationFactor = 15,
28 | isRevese = false,
29 | springOptions,
30 | }: TiltProps) {
31 | const ref = useRef(null);
32 |
33 | const x = useMotionValue(0);
34 | const y = useMotionValue(0);
35 |
36 | const xSpring = useSpring(x, springOptions);
37 | const ySpring = useSpring(y, springOptions);
38 |
39 | const rotateX = useTransform(
40 | ySpring,
41 | [-0.5, 0.5],
42 | isRevese
43 | ? [rotationFactor, -rotationFactor]
44 | : [-rotationFactor, rotationFactor]
45 | );
46 | const rotateY = useTransform(
47 | xSpring,
48 | [-0.5, 0.5],
49 | isRevese
50 | ? [-rotationFactor, rotationFactor]
51 | : [rotationFactor, -rotationFactor]
52 | );
53 |
54 | const transform = useMotionTemplate`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
55 |
56 | const handleMouseMove = (e: React.MouseEvent) => {
57 | if (!ref.current) return;
58 |
59 | const rect = ref.current.getBoundingClientRect();
60 | const width = rect.width;
61 | const height = rect.height;
62 | const mouseX = e.clientX - rect.left;
63 | const mouseY = e.clientY - rect.top;
64 |
65 | const xPos = mouseX / width - 0.5;
66 | const yPos = mouseY / height - 0.5;
67 |
68 | x.set(xPos);
69 | y.set(yPos);
70 | };
71 |
72 | const handleMouseLeave = () => {
73 | x.set(0);
74 | y.set(0);
75 | };
76 |
77 | return (
78 |
89 | {children}
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/components/core/transition-panel.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import {
3 | AnimatePresence,
4 | Transition,
5 | Variant,
6 | motion,
7 | MotionProps,
8 | } from 'motion/react';
9 | import { cn } from '@/lib/utils';
10 |
11 | export type TransitionPanelProps = {
12 | children: React.ReactNode[];
13 | className?: string;
14 | transition?: Transition;
15 | activeIndex: number;
16 | variants?: { enter: Variant; center: Variant; exit: Variant };
17 | } & MotionProps;
18 |
19 | export function TransitionPanel({
20 | children,
21 | className,
22 | transition,
23 | variants,
24 | activeIndex,
25 | ...motionProps
26 | }: TransitionPanelProps) {
27 | return (
28 |
29 |
34 |
43 | {children[activeIndex]}
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',
16 | outline:
17 | 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
20 | ghost: 'hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 hover:underline',
22 | },
23 | size: {
24 | default: 'h-9 px-4 py-2 has-[>svg]:px-3',
25 | sm: 'h-8 rounded-md px-3 has-[>svg]:px-2.5',
26 | lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
27 | icon: 'size-9',
28 | },
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default',
33 | },
34 | }
35 | );
36 |
37 | function Button({
38 | className,
39 | variant,
40 | size,
41 | asChild = false,
42 | ...props
43 | }: React.ComponentProps<'button'> &
44 | VariantProps & {
45 | asChild?: boolean;
46 | }) {
47 | const Comp = asChild ? Slot : 'button';
48 |
49 | return (
50 |
55 | );
56 | }
57 |
58 | export { Button, buttonVariants };
59 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function Tooltip({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function TooltipTrigger({
32 | ...props
33 | }: React.ComponentProps) {
34 | return
35 | }
36 |
37 | function TooltipContent({
38 | className,
39 | sideOffset = 0,
40 | children,
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
54 | {children}
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
62 |
--------------------------------------------------------------------------------
/components/website/card-example-landing.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import { cloneElement, useState } from 'react';
4 | import { RotateCw } from 'lucide-react';
5 |
6 | export function CardExampleLanding({
7 | children,
8 | className,
9 | hasReTrigger,
10 | }: {
11 | children: React.ReactElement;
12 | className?: string;
13 | hasReTrigger?: boolean;
14 | }) {
15 | const [reTriggerKey, setReTriggerKey] = useState(Date.now());
16 |
17 | const reTrigger = () => {
18 | setReTriggerKey(Date.now());
19 | };
20 |
21 | return (
22 |
23 |
24 |
30 | {hasReTrigger && (
31 |
35 |
36 |
37 | )}
38 |
39 | {hasReTrigger
40 | ? cloneElement(children, { key: reTriggerKey })
41 | : children}
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/components/website/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { extractCodeFromFilePath } from '@/lib/code';
2 | import CodePreview from './code-preview';
3 | import CodeRenderer from './code-renderer';
4 | import { cn } from '@/lib/utils';
5 |
6 | type CodeBlockProps = {
7 | filePath?: string;
8 | code?: string;
9 | lang?: string;
10 | className?: string;
11 | };
12 |
13 | export default function CodeBlock({
14 | filePath,
15 | code = '',
16 | lang = 'tsx',
17 | className,
18 | }: CodeBlockProps) {
19 | const fileContent = filePath ? extractCodeFromFilePath(filePath) : code;
20 |
21 | return (
22 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components/website/code-preview.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Check, Copy } from 'lucide-react';
3 | import { useState } from 'react';
4 |
5 | type CodePreviewProps = {
6 | code: string;
7 | children: React.ReactNode;
8 | };
9 |
10 | export default function CodePreview({ code, children }: CodePreviewProps) {
11 | const [hasCheckIcon, setHasCheckIcon] = useState(false);
12 |
13 | const onCopy = () => {
14 | navigator.clipboard.writeText(code);
15 | setHasCheckIcon(true);
16 |
17 | setTimeout(() => {
18 | setHasCheckIcon(false);
19 | }, 1000);
20 | };
21 |
22 | return (
23 |
24 |
28 |
33 |
34 |
35 |
40 |
41 |
42 |
43 |
44 |
45 | {children}
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/website/code-renderer.tsx:
--------------------------------------------------------------------------------
1 | import { codeToHtml } from '@/lib/shiki';
2 |
3 | type CodeRenderer = {
4 | code: string;
5 | lang: string;
6 | };
7 |
8 | export default async function CodeRenderer({ code, lang }: CodeRenderer) {
9 | const html = await codeToHtml({
10 | code,
11 | lang,
12 | });
13 |
14 | return (
15 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/website/component-code-preview.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs';
2 | import CodePreview from './code-preview';
3 | import CodeRenderer from './code-renderer';
4 | import ComponentPreview from './component-preview';
5 | import { extractCodeFromFilePath } from '@/lib/code';
6 |
7 | type ComponentCodePreview = {
8 | component: React.ReactElement;
9 | filePath: string;
10 | hasReTrigger?: boolean;
11 | classNameComponentContainer?: string;
12 | };
13 |
14 | export default function ComponentCodePreview({
15 | component,
16 | filePath,
17 | hasReTrigger,
18 | classNameComponentContainer,
19 | }: ComponentCodePreview) {
20 | const fileContent = extractCodeFromFilePath(filePath);
21 |
22 | return (
23 |
24 |
25 |
26 | Preview
27 | Code
28 |
29 |
33 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/components/website/component-preview.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { cn } from '@/lib/utils';
3 | import { RotateCw } from 'lucide-react';
4 | import { OpenInV0Button } from './open-in-v0';
5 | import { cloneElement, useState } from 'react';
6 | import { Tooltip, TooltipContent } from '../ui/tooltip';
7 | import { TooltipTrigger } from '../ui/tooltip';
8 |
9 | type ComponentPreviewProps = {
10 | component: React.ReactElement;
11 | hasReTrigger?: boolean;
12 | className?: string;
13 | filePath?: string;
14 | };
15 |
16 | export default function ComponentPreview({
17 | component,
18 | hasReTrigger = false,
19 | className,
20 | filePath,
21 | }: ComponentPreviewProps) {
22 | const [reTriggerKey, setReTriggerKey] = useState(Date.now());
23 |
24 | const componentName = filePath?.split('/').pop()?.split('.').shift();
25 | const registryUrl = `https://motion-primitives.com/e/${componentName}.json`;
26 |
27 | const reTrigger = () => {
28 | setReTriggerKey(Date.now());
29 | };
30 |
31 | return (
32 |
38 |
39 |
40 |
41 | {hasReTrigger && (
42 |
43 |
44 |
49 |
50 |
51 |
52 | Re-trigger
53 |
54 | )}
55 |
56 |
57 | {hasReTrigger
58 | ? cloneElement(component, { key: reTriggerKey })
59 | : component}
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/components/website/icons/github.tsx:
--------------------------------------------------------------------------------
1 | export default function GitHubIcon(props: React.SVGProps) {
2 | return (
3 |
9 | GitHub
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/components/website/icons/motion-primitives-logo.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as React from 'react';
3 | import type { SVGProps } from 'react';
4 |
5 | export function MPLogo(props: SVGProps) {
6 | return (
7 |
18 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/website/icons/shadcn-logo.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as React from 'react';
3 | import type { SVGProps } from 'react';
4 |
5 | export function ShadcnLogo(props: SVGProps) {
6 | return (
7 |
16 |
17 |
27 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/components/website/icons/x.tsx:
--------------------------------------------------------------------------------
1 | export default function XIcon(props: React.SVGProps) {
2 | return (
3 |
9 | X
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/components/website/launch-banner.tsx:
--------------------------------------------------------------------------------
1 | export default function LaunchBanner() {
2 | return (
3 |
8 |
9 | Motion-Primitives Pro is live - Advanced components and templates to
10 | help you build delightful websites, faster.
11 | {' '}
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/components/website/open-in-v0.tsx:
--------------------------------------------------------------------------------
1 | import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
2 |
3 | export function OpenInV0Button({ url }: { url: string }) {
4 | return (
5 |
6 |
7 |
12 |
17 |
23 |
27 |
31 |
32 |
33 |
34 |
35 | Open in v0
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/components/website/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import * as React from 'react';
3 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
4 | import { cn } from '@/lib/utils';
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 | ));
22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
23 |
24 | const ScrollBar = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, orientation = 'vertical', ...props }, ref) => (
28 |
41 |
42 |
43 | ));
44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
45 |
46 | export { ScrollArea, ScrollBar };
47 |
--------------------------------------------------------------------------------
/components/website/table-of-contents.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect, useState } from 'react';
3 | import { usePathname } from 'next/navigation';
4 | import { cn } from '@/lib/utils';
5 |
6 | type Heading = {
7 | id: string;
8 | text: string;
9 | level: number;
10 | link: string;
11 | };
12 |
13 | // can be improved
14 | export function TableOfContents() {
15 | const [headings, setHeadings] = useState([]);
16 | const pathname = usePathname();
17 |
18 | useEffect(() => {
19 | const updateHeadings = () => {
20 | const elements = Array.from(document.querySelectorAll('[data-heading]'));
21 | const newHeadings = elements
22 | .map((elem) => {
23 | const level = parseInt(elem.getAttribute('data-heading') || '2', 10);
24 |
25 | if (level === 1) return null;
26 |
27 | return {
28 | id: `${elem.id}-${level}`,
29 | link: elem.id,
30 | text: elem.textContent ?? '',
31 | level,
32 | };
33 | })
34 | .filter((heading): heading is Heading => heading !== null);
35 | setHeadings(newHeadings);
36 | };
37 |
38 | updateHeadings();
39 |
40 | const observer = new MutationObserver(updateHeadings);
41 | observer.observe(document.body, { childList: true, subtree: true });
42 |
43 | return () => observer.disconnect();
44 | }, [pathname]);
45 |
46 | if (headings.length === 0) return null;
47 |
48 | return (
49 | <>
50 |
51 | On this page
52 |
53 |
77 | >
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/components/website/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes';
5 | import { type ThemeProviderProps } from 'next-themes/dist/types';
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children} ;
9 | }
10 |
--------------------------------------------------------------------------------
/components/website/theme-switch.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useTheme } from 'next-themes';
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuTrigger,
9 | } from './dropdown-menu';
10 | import { MoonIcon, SunIcon } from 'lucide-react';
11 |
12 | export default function ThemeSwitch() {
13 | const { theme, setTheme } = useTheme();
14 |
15 | return (
16 |
17 |
18 |
23 | {theme === 'light' ? (
24 |
25 | ) : (
26 |
27 | )}
28 | Toggle theme
29 |
30 |
31 |
32 | setTheme('light')}>
33 | Light
34 |
35 | setTheme('dark')}>
36 | Dark
37 |
38 | setTheme('system')}>
39 | System
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/hooks/useClickOutside.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react';
2 |
3 | function useClickOutside(
4 | ref: RefObject,
5 | handler: (event: MouseEvent | TouchEvent) => void
6 | ): void {
7 | useEffect(() => {
8 | const handleClickOutside = (event: MouseEvent | TouchEvent) => {
9 | if (!ref || !ref.current || ref.current.contains(event.target as Node)) {
10 | return;
11 | }
12 |
13 | handler(event);
14 | };
15 |
16 | document.addEventListener('mousedown', handleClickOutside);
17 | document.addEventListener('touchstart', handleClickOutside);
18 |
19 | return () => {
20 | document.removeEventListener('mousedown', handleClickOutside);
21 | document.removeEventListener('touchstart', handleClickOutside);
22 | };
23 | }, [ref, handler]);
24 | }
25 |
26 | export default useClickOutside;
27 |
--------------------------------------------------------------------------------
/lib/browser.ts:
--------------------------------------------------------------------------------
1 | export function isMobileFirefox(): boolean | undefined {
2 | const userAgent = navigator.userAgent;
3 | return (
4 | typeof window !== 'undefined' &&
5 | ((/Firefox/.test(userAgent) && /Mobile/.test(userAgent)) || // Android Firefox
6 | /FxiOS/.test(userAgent)) // iOS Firefox
7 | );
8 | }
9 |
10 | export function isMac(): boolean | undefined {
11 | return testPlatform(/^Mac/);
12 | }
13 |
14 | export function isIPhone(): boolean | undefined {
15 | return testPlatform(/^iPhone/);
16 | }
17 |
18 | export function isSafari(): boolean | undefined {
19 | return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
20 | }
21 |
22 | export function isIPad(): boolean | undefined {
23 | return (
24 | testPlatform(/^iPad/) ||
25 | // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
26 | (isMac() && navigator.maxTouchPoints > 1)
27 | );
28 | }
29 |
30 | export function isIOS(): boolean | undefined {
31 | return isIPhone() || isIPad();
32 | }
33 |
34 | export function testPlatform(re: RegExp): boolean | undefined {
35 | return typeof window !== 'undefined' && window.navigator != null
36 | ? re.test(window.navigator.platform)
37 | : undefined;
38 | }
39 |
--------------------------------------------------------------------------------
/lib/code.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 |
3 | export const extractCodeFromFilePath = (filePath: string) => {
4 | const fileContent = fs.readFileSync(filePath, 'utf-8');
5 |
6 | return fileContent;
7 | };
8 |
--------------------------------------------------------------------------------
/lib/custom-theme.ts:
--------------------------------------------------------------------------------
1 | import { createCssVariablesTheme } from './theme-css-variables';
2 |
3 | export const noir = createCssVariablesTheme({
4 | name: 'noir',
5 | variableDefaults: {
6 | foreground: '#B1B1B1',
7 | background: '#18181b',
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/lib/shiki.ts:
--------------------------------------------------------------------------------
1 | import { bundledLanguages, createHighlighter } from 'shiki/bundle/web';
2 | import { noir } from './custom-theme';
3 |
4 | export const codeToHtml = async ({
5 | code,
6 | lang,
7 | }: {
8 | code: string;
9 | lang: string;
10 | }) => {
11 | const highlighter = await createHighlighter({
12 | themes: [noir],
13 | langs: [...Object.keys(bundledLanguages)],
14 | });
15 |
16 | return highlighter.codeToHtml(code, {
17 | lang: lang,
18 | theme: 'noir',
19 | });
20 | };
21 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import remarkGfm from 'remark-gfm';
2 | import createMDX from '@next/mdx';
3 | import { remarkCodeHike } from '@code-hike/mdx';
4 |
5 | /** @type {import('next').NextConfig} */
6 | const nextConfig = {
7 | reactStrictMode: true,
8 | pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
9 | };
10 |
11 | const withMDX = createMDX({
12 | extension: /\.mdx?$/,
13 | options: {
14 | remarkPlugins: [remarkGfm, [remarkCodeHike, { theme: 'css-variables' }]],
15 | },
16 | });
17 |
18 | export default withMDX(nextConfig);
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "motion-primitives",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "build:registry": "tsx scripts/registry-build.ts"
11 | },
12 | "dependencies": {
13 | "@code-hike/mdx": "^0.9.0",
14 | "@mdx-js/loader": "^3.0.1",
15 | "@mdx-js/react": "^3.0.1",
16 | "@next/mdx": "^14.2.4",
17 | "@radix-ui/react-dropdown-menu": "^2.1.1",
18 | "@radix-ui/react-label": "^2.1.2",
19 | "@radix-ui/react-scroll-area": "^1.1.0",
20 | "@radix-ui/react-slot": "^1.1.2",
21 | "@radix-ui/react-tabs": "^1.1.3",
22 | "@radix-ui/react-tooltip": "^1.1.8",
23 | "@types/mdx": "^2.0.13",
24 | "class-variance-authority": "^0.7.1",
25 | "clsx": "^2.1.1",
26 | "geist": "^1.3.1",
27 | "glob": "^11.0.1",
28 | "lucide-react": "^0.400.0",
29 | "motion": "^11.12.0",
30 | "next": "^14.2.14",
31 | "next-themes": "^0.3.0",
32 | "react": "^18",
33 | "react-dom": "^18",
34 | "react-use-measure": "^2.1.1",
35 | "remark-gfm": "^4.0.0",
36 | "shiki": "^1.10.1",
37 | "tailwind-merge": "^2.6.0",
38 | "tailwindcss-animate": "^1.0.7"
39 | },
40 | "devDependencies": {
41 | "@tailwindcss/postcss": "^4.0.0",
42 | "@tailwindcss/typography": "^0.5.16",
43 | "@types/node": "^20.17.9",
44 | "@types/react": "^18",
45 | "@types/react-dom": "^18",
46 | "eslint": "^8",
47 | "eslint-config-next": "14.2.4",
48 | "postcss": "^8",
49 | "prettier": "^3.3.3",
50 | "prettier-plugin-tailwindcss": "^0.0.0-insiders.6d3fa07",
51 | "tailwindcss": "^4.0.0",
52 | "tsx": "^4.19.2",
53 | "typescript": "^5"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | '@tailwindcss/postcss': {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | trailingComma: 'es5',
4 | semi: true,
5 | tabWidth: 2,
6 | singleQuote: true,
7 | jsxSingleQuote: true,
8 | plugins: ['prettier-plugin-tailwindcss'],
9 | };
10 |
--------------------------------------------------------------------------------
/public/c/animated-number.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "animated-number",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "animated-number.tsx",
17 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { motion, SpringOptions, useSpring, useTransform } from 'motion/react';\nimport { useEffect } from 'react';\n\nexport type AnimatedNumberProps = {\n value: number;\n className?: string;\n springOptions?: SpringOptions;\n as?: React.ElementType;\n};\n\nexport function AnimatedNumber({\n value,\n className,\n springOptions,\n as = 'span',\n}: AnimatedNumberProps) {\n const MotionComponent = motion.create(as as keyof JSX.IntrinsicElements);\n\n const spring = useSpring(value, springOptions);\n const display = useTransform(spring, (current) =>\n Math.round(current).toLocaleString()\n );\n\n useEffect(() => {\n spring.set(value);\n }, [spring, value]);\n\n return (\n \n {display}\n \n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/border-trail.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "border-trail",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "border-trail.tsx",
17 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { motion, Transition } from 'motion/react';\n\nexport type BorderTrailProps = {\n className?: string;\n size?: number;\n transition?: Transition;\n onAnimationComplete?: () => void;\n style?: React.CSSProperties;\n};\n\nexport function BorderTrail({\n className,\n size = 60,\n transition,\n onAnimationComplete,\n style,\n}: BorderTrailProps) {\n const defaultTransition: Transition = {\n repeat: Infinity,\n duration: 5,\n ease: 'linear',\n };\n\n return (\n \n \n
\n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/in-view.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "in-view",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "in-view.tsx",
17 | "content": "'use client';\nimport { ReactNode, useRef } from 'react';\nimport {\n motion,\n useInView,\n Variant,\n Transition,\n UseInViewOptions,\n} from 'motion/react';\n\nexport type InViewProps = {\n children: ReactNode;\n variants?: {\n hidden: Variant;\n visible: Variant;\n };\n transition?: Transition;\n viewOptions?: UseInViewOptions;\n as?: React.ElementType;\n};\n\nconst defaultVariants = {\n hidden: { opacity: 0 },\n visible: { opacity: 1 },\n};\n\nexport function InView({\n children,\n variants = defaultVariants,\n transition,\n viewOptions,\n as = 'div',\n}: InViewProps) {\n const ref = useRef(null);\n const isInView = useInView(ref, viewOptions);\n\n const MotionComponent = motion[as as keyof typeof motion] as typeof as;\n\n return (\n \n {children}\n \n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/progressive-blur.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "progressive-blur",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "progressive-blur.tsx",
17 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { HTMLMotionProps, motion } from 'motion/react';\n\nexport const GRADIENT_ANGLES = {\n top: 0,\n right: 90,\n bottom: 180,\n left: 270,\n};\n\nexport type ProgressiveBlurProps = {\n direction?: keyof typeof GRADIENT_ANGLES;\n blurLayers?: number;\n className?: string;\n blurIntensity?: number;\n} & HTMLMotionProps<'div'>;\n\nexport function ProgressiveBlur({\n direction = 'bottom',\n blurLayers = 8,\n className,\n blurIntensity = 0.25,\n ...props\n}: ProgressiveBlurProps) {\n const layers = Math.max(blurLayers, 2);\n const segmentSize = 1 / (blurLayers + 1);\n\n return (\n \n {Array.from({ length: layers }).map((_, index) => {\n const angle = GRADIENT_ANGLES[direction];\n const gradientStops = [\n index * segmentSize,\n (index + 1) * segmentSize,\n (index + 2) * segmentSize,\n (index + 3) * segmentSize,\n ].map(\n (pos, posIndex) =>\n `rgba(255, 255, 255, ${posIndex === 1 || posIndex === 2 ? 1 : 0}) ${pos * 100}%`\n );\n\n const gradient = `linear-gradient(${angle}deg, ${gradientStops.join(\n ', '\n )})`;\n\n return (\n \n );\n })}\n
\n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/scroll-progress.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scroll-progress",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "scroll-progress.tsx",
17 | "content": "'use client';\n\nimport { motion, SpringOptions, useScroll, useSpring } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { RefObject } from 'react';\n\nexport type ScrollProgressProps = {\n className?: string;\n springOptions?: SpringOptions;\n containerRef?: RefObject;\n};\n\nconst DEFAULT_SPRING_OPTIONS: SpringOptions = {\n stiffness: 200,\n damping: 50,\n restDelta: 0.001,\n};\n\nexport function ScrollProgress({\n className,\n springOptions,\n containerRef,\n}: ScrollProgressProps) {\n const { scrollYProgress } = useScroll({\n container: containerRef,\n layoutEffect: Boolean(containerRef?.current),\n });\n\n const scaleX = useSpring(scrollYProgress, {\n ...DEFAULT_SPRING_OPTIONS,\n ...(springOptions ?? {}),\n });\n\n return (\n \n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/text-loop.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text-loop",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "text-loop.tsx",
17 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport {\n motion,\n AnimatePresence,\n Transition,\n Variants,\n AnimatePresenceProps,\n} from 'motion/react';\nimport { useState, useEffect, Children } from 'react';\n\nexport type TextLoopProps = {\n children: React.ReactNode[];\n className?: string;\n interval?: number;\n transition?: Transition;\n variants?: Variants;\n onIndexChange?: (index: number) => void;\n trigger?: boolean;\n mode?: AnimatePresenceProps['mode'];\n};\n\nexport function TextLoop({\n children,\n className,\n interval = 2,\n transition = { duration: 0.3 },\n variants,\n onIndexChange,\n trigger = true,\n mode = 'popLayout',\n}: TextLoopProps) {\n const [currentIndex, setCurrentIndex] = useState(0);\n const items = Children.toArray(children);\n\n useEffect(() => {\n if (!trigger) return;\n\n const intervalMs = interval * 1000;\n const timer = setInterval(() => {\n setCurrentIndex((current) => {\n const next = (current + 1) % items.length;\n onIndexChange?.(next);\n return next;\n });\n }, intervalMs);\n return () => clearInterval(timer);\n }, [items.length, interval, onIndexChange, trigger]);\n\n const motionVariants: Variants = {\n initial: { y: 20, opacity: 0 },\n animate: { y: 0, opacity: 1 },\n exit: { y: -20, opacity: 0 },\n };\n\n return (\n \n
\n \n {items[currentIndex]}\n \n \n
\n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/text-morph.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text-morph",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "text-morph.tsx",
17 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { AnimatePresence, motion, Transition, Variants } from 'motion/react';\nimport { useMemo, useId } from 'react';\n\nexport type TextMorphProps = {\n children: string;\n as?: React.ElementType;\n className?: string;\n style?: React.CSSProperties;\n variants?: Variants;\n transition?: Transition;\n};\n\nexport function TextMorph({\n children,\n as: Component = 'p',\n className,\n style,\n variants,\n transition,\n}: TextMorphProps) {\n const uniqueId = useId();\n\n const characters = useMemo(() => {\n const charCounts: Record = {};\n\n return children.split('').map((char) => {\n const lowerChar = char.toLowerCase();\n charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;\n\n return {\n id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,\n label: char === ' ' ? '\\u00A0' : char,\n };\n });\n }, [children, uniqueId]);\n\n const defaultVariants: Variants = {\n initial: { opacity: 0 },\n animate: { opacity: 1 },\n exit: { opacity: 0 },\n };\n\n const defaultTransition: Transition = {\n type: 'spring',\n stiffness: 280,\n damping: 18,\n mass: 0.3,\n };\n\n return (\n \n \n {characters.map((character) => (\n \n {character.label}\n \n ))}\n \n \n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/text-scramble.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text-scramble",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "text-scramble.tsx",
17 | "content": "'use client';\nimport { type JSX, useEffect, useState } from 'react';\nimport { motion, MotionProps } from 'motion/react';\n\nexport type TextScrambleProps = {\n children: string;\n duration?: number;\n speed?: number;\n characterSet?: string;\n as?: React.ElementType;\n className?: string;\n trigger?: boolean;\n onScrambleComplete?: () => void;\n} & MotionProps;\n\nconst defaultChars =\n 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\nexport function TextScramble({\n children,\n duration = 0.8,\n speed = 0.04,\n characterSet = defaultChars,\n className,\n as: Component = 'p',\n trigger = true,\n onScrambleComplete,\n ...props\n}: TextScrambleProps) {\n const MotionComponent = motion.create(\n Component as keyof JSX.IntrinsicElements\n );\n const [displayText, setDisplayText] = useState(children);\n const [isAnimating, setIsAnimating] = useState(false);\n const text = children;\n\n const scramble = async () => {\n if (isAnimating) return;\n setIsAnimating(true);\n\n const steps = duration / speed;\n let step = 0;\n\n const interval = setInterval(() => {\n let scrambled = '';\n const progress = step / steps;\n\n for (let i = 0; i < text.length; i++) {\n if (text[i] === ' ') {\n scrambled += ' ';\n continue;\n }\n\n if (progress * text.length > i) {\n scrambled += text[i];\n } else {\n scrambled +=\n characterSet[Math.floor(Math.random() * characterSet.length)];\n }\n }\n\n setDisplayText(scrambled);\n step++;\n\n if (step > steps) {\n clearInterval(interval);\n setDisplayText(text);\n setIsAnimating(false);\n onScrambleComplete?.();\n }\n }, speed * 1000);\n };\n\n useEffect(() => {\n if (!trigger) return;\n\n scramble();\n }, [trigger]);\n\n return (\n \n {displayText}\n \n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/text-shimmer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text-shimmer",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "text-shimmer.tsx",
17 | "content": "'use client';\nimport React, { useMemo, type JSX } from 'react';\nimport { motion } from 'motion/react';\nimport { cn } from '@/lib/utils';\n\nexport type TextShimmerProps = {\n children: string;\n as?: React.ElementType;\n className?: string;\n duration?: number;\n spread?: number;\n};\n\nfunction TextShimmerComponent({\n children,\n as: Component = 'p',\n className,\n duration = 2,\n spread = 2,\n}: TextShimmerProps) {\n const MotionComponent = motion.create(\n Component as keyof JSX.IntrinsicElements\n );\n\n const dynamicSpread = useMemo(() => {\n return children.length * spread;\n }, [children, spread]);\n\n return (\n \n {children}\n \n );\n}\n\nexport const TextShimmer = React.memo(TextShimmerComponent);\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/tilt.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tilt",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "tilt.tsx",
17 | "content": "'use client';\n\nimport React, { useRef } from 'react';\nimport {\n motion,\n useMotionTemplate,\n useMotionValue,\n useSpring,\n useTransform,\n MotionStyle,\n SpringOptions,\n} from 'motion/react';\n\nexport type TiltProps = {\n children: React.ReactNode;\n className?: string;\n style?: MotionStyle;\n rotationFactor?: number;\n isRevese?: boolean;\n springOptions?: SpringOptions;\n};\n\nexport function Tilt({\n children,\n className,\n style,\n rotationFactor = 15,\n isRevese = false,\n springOptions,\n}: TiltProps) {\n const ref = useRef(null);\n\n const x = useMotionValue(0);\n const y = useMotionValue(0);\n\n const xSpring = useSpring(x, springOptions);\n const ySpring = useSpring(y, springOptions);\n\n const rotateX = useTransform(\n ySpring,\n [-0.5, 0.5],\n isRevese\n ? [rotationFactor, -rotationFactor]\n : [-rotationFactor, rotationFactor]\n );\n const rotateY = useTransform(\n xSpring,\n [-0.5, 0.5],\n isRevese\n ? [-rotationFactor, rotationFactor]\n : [rotationFactor, -rotationFactor]\n );\n\n const transform = useMotionTemplate`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;\n\n const handleMouseMove = (e: React.MouseEvent) => {\n if (!ref.current) return;\n\n const rect = ref.current.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n const mouseX = e.clientX - rect.left;\n const mouseY = e.clientY - rect.top;\n\n const xPos = mouseX / width - 0.5;\n const yPos = mouseY / height - 0.5;\n\n x.set(xPos);\n y.set(yPos);\n };\n\n const handleMouseLeave = () => {\n x.set(0);\n y.set(0);\n };\n\n return (\n \n {children}\n \n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/c/transition-panel.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transition-panel",
3 | "type": "registry:ui",
4 | "registryDependencies": [],
5 | "dependencies": [
6 | "motion"
7 | ],
8 | "devDependencies": [],
9 | "tailwind": {},
10 | "cssVars": {
11 | "light": {},
12 | "dark": {}
13 | },
14 | "files": [
15 | {
16 | "path": "transition-panel.tsx",
17 | "content": "'use client';\nimport {\n AnimatePresence,\n Transition,\n Variant,\n motion,\n MotionProps,\n} from 'motion/react';\nimport { cn } from '@/lib/utils';\n\nexport type TransitionPanelProps = {\n children: React.ReactNode[];\n className?: string;\n transition?: Transition;\n activeIndex: number;\n variants?: { enter: Variant; center: Variant; exit: Variant };\n} & MotionProps;\n\nexport function TransitionPanel({\n children,\n className,\n transition,\n variants,\n activeIndex,\n ...motionProps\n}: TransitionPanelProps) {\n return (\n \n
\n \n {children[activeIndex]}\n \n \n
\n );\n}\n",
18 | "type": "registry:ui"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/public/e/animated-number-basic.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "animated-number-basic",
3 | "type": "registry:ui",
4 | "componentName": "animated-number-basic",
5 | "description": "Basic implementation of the animated number component.",
6 | "files": [
7 | {
8 | "path": "animated-number-basic.tsx",
9 | "content": "'use client';\nimport { useEffect, useState } from 'react';\nimport { AnimatedNumber } from '@/components/core/animated-number';\n\nexport function AnimatedNumberBasic() {\n const [value, setValue] = useState(0);\n\n useEffect(() => {\n setValue(2082);\n }, []);\n\n return (\n \n );\n}\n",
10 | "type": "registry:component"
11 | },
12 | {
13 | "path": "components/core/animated-number.tsx",
14 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { motion, SpringOptions, useSpring, useTransform } from 'motion/react';\nimport { useEffect } from 'react';\n\nexport type AnimatedNumberProps = {\n value: number;\n className?: string;\n springOptions?: SpringOptions;\n as?: React.ElementType;\n};\n\nexport function AnimatedNumber({\n value,\n className,\n springOptions,\n as = 'span',\n}: AnimatedNumberProps) {\n const MotionComponent = motion.create(as as keyof JSX.IntrinsicElements);\n\n const spring = useSpring(value, springOptions);\n const display = useTransform(spring, (current) =>\n Math.round(current).toLocaleString()\n );\n\n useEffect(() => {\n spring.set(value);\n }, [spring, value]);\n\n return (\n \n {display}\n \n );\n}\n",
15 | "type": "registry:ui"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/public/e/animated-number-counter.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "animated-number-counter",
3 | "type": "registry:ui",
4 | "componentName": "animated-number-counter",
5 | "description": "Animated counter implementation that smoothly transitions between numbers.",
6 | "files": [
7 | {
8 | "path": "animated-number-counter.tsx",
9 | "content": "'use client';\nimport { useState } from 'react';\nimport { AnimatedNumber } from '@/components/core/animated-number';\nimport { Minus, Plus } from 'lucide-react';\n\nexport function AnimatedNumberCounter() {\n const [value, setValue] = useState(1000);\n\n return (\n \n
setValue((prev) => prev - 100)}\n >\n \n \n
\n
setValue((prev) => prev + 100)}\n >\n \n \n
\n );\n}\n",
10 | "type": "registry:component"
11 | },
12 | {
13 | "path": "components/core/animated-number.tsx",
14 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { motion, SpringOptions, useSpring, useTransform } from 'motion/react';\nimport { useEffect } from 'react';\n\nexport type AnimatedNumberProps = {\n value: number;\n className?: string;\n springOptions?: SpringOptions;\n as?: React.ElementType;\n};\n\nexport function AnimatedNumber({\n value,\n className,\n springOptions,\n as = 'span',\n}: AnimatedNumberProps) {\n const MotionComponent = motion.create(as as keyof JSX.IntrinsicElements);\n\n const spring = useSpring(value, springOptions);\n const display = useTransform(spring, (current) =>\n Math.round(current).toLocaleString()\n );\n\n useEffect(() => {\n spring.set(value);\n }, [spring, value]);\n\n return (\n \n {display}\n \n );\n}\n",
15 | "type": "registry:ui"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/public/e/animated-number-in-view.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "animated-number-in-view",
3 | "type": "registry:ui",
4 | "componentName": "animated-number-in-view",
5 | "description": "Animated number that activates when scrolled into view.",
6 | "files": [
7 | {
8 | "path": "animated-number-in-view.tsx",
9 | "content": "'use client';\nimport { AnimatedNumber } from '@/components/core/animated-number';\nimport { useInView } from 'motion/react';\nimport { useRef, useState } from 'react';\n\nexport function AnimatedNumberInView() {\n const [value, setValue] = useState(0);\n const ref = useRef(null);\n const isInView = useInView(ref);\n\n if (isInView && value === 0) {\n setValue(10000);\n }\n\n return (\n \n );\n}\n",
10 | "type": "registry:component"
11 | },
12 | {
13 | "path": "components/core/animated-number.tsx",
14 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { motion, SpringOptions, useSpring, useTransform } from 'motion/react';\nimport { useEffect } from 'react';\n\nexport type AnimatedNumberProps = {\n value: number;\n className?: string;\n springOptions?: SpringOptions;\n as?: React.ElementType;\n};\n\nexport function AnimatedNumber({\n value,\n className,\n springOptions,\n as = 'span',\n}: AnimatedNumberProps) {\n const MotionComponent = motion.create(as as keyof JSX.IntrinsicElements);\n\n const spring = useSpring(value, springOptions);\n const display = useTransform(spring, (current) =>\n Math.round(current).toLocaleString()\n );\n\n useEffect(() => {\n spring.set(value);\n }, [spring, value]);\n\n return (\n \n {display}\n \n );\n}\n",
15 | "type": "registry:ui"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/public/e/border-trail-textarea.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "border-trail-textarea",
3 | "type": "registry:ui",
4 | "componentName": "border-trail-textarea",
5 | "description": "Text area input with animated border trail effect on focus.",
6 | "files": [
7 | {
8 | "path": "border-trail-textarea.tsx",
9 | "content": "import { BorderTrail } from '@/components/core/border-trail';\n\nexport function BorderTrailTextarea() {\n return (\n \n \n \n
\n );\n}\n",
10 | "type": "registry:component"
11 | },
12 | {
13 | "path": "components/core/border-trail.tsx",
14 | "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { motion, Transition } from 'motion/react';\n\nexport type BorderTrailProps = {\n className?: string;\n size?: number;\n transition?: Transition;\n onAnimationComplete?: () => void;\n style?: React.CSSProperties;\n};\n\nexport function BorderTrail({\n className,\n size = 60,\n transition,\n onAnimationComplete,\n style,\n}: BorderTrailProps) {\n const defaultTransition: Transition = {\n repeat: Infinity,\n duration: 5,\n ease: 'linear',\n };\n\n return (\n \n \n
\n );\n}\n",
15 | "type": "registry:ui"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/public/e/glow-effect-button.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glow-effect-button",
3 | "type": "registry:ui",
4 | "componentName": "glow-effect-button",
5 | "description": "Button with interactive glow effect on hover.",
6 | "files": [
7 | {
8 | "path": "glow-effect-button.tsx",
9 | "content": "import { GlowEffect } from '@/components/core/glow-effect';\nimport { ArrowRight } from 'lucide-react';\n\nexport function GlowEffectButton() {\n return (\n \n );\n}\n",
10 | "type": "registry:component"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/public/e/text-shimmer-basic.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text-shimmer-basic",
3 | "type": "registry:ui",
4 | "componentName": "text-shimmer",
5 | "description": "Basic implementation of the text shimmer effect.",
6 | "files": [
7 | {
8 | "path": "text-shimmer-basic.tsx",
9 | "content": "import { TextShimmer } from '@/components/core/text-shimmer';\n\nexport function TextShimmerBasic() {\n return (\n \n Generating code...\n \n );\n}\n",
10 | "type": "registry:component"
11 | },
12 | {
13 | "path": "components/core/text-shimmer.tsx",
14 | "content": "'use client';\nimport React, { useMemo, type JSX } from 'react';\nimport { motion } from 'motion/react';\nimport { cn } from '@/lib/utils';\n\nexport type TextShimmerProps = {\n children: string;\n as?: React.ElementType;\n className?: string;\n duration?: number;\n spread?: number;\n};\n\nfunction TextShimmerComponent({\n children,\n as: Component = 'p',\n className,\n duration = 2,\n spread = 2,\n}: TextShimmerProps) {\n const MotionComponent = motion.create(\n Component as keyof JSX.IntrinsicElements\n );\n\n const dynamicSpread = useMemo(() => {\n return children.length * spread;\n }, [children, spread]);\n\n return (\n \n {children}\n \n );\n}\n\nexport const TextShimmer = React.memo(TextShimmerComponent);\n",
15 | "type": "registry:ui"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/public/eb-27-lamp-edouard-wilfrid-buquet.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibelick/motion-primitives/ab4563f012c063e542de3babe3b901a8b521a7de/public/eb-27-lamp-edouard-wilfrid-buquet.jpg
--------------------------------------------------------------------------------
/public/gucci_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/h/useClickOutside.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "useClickOutside",
3 | "type": "registry:hook",
4 | "files": [
5 | {
6 | "path": "useClickOutside.ts",
7 | "content": "import { RefObject, useEffect } from 'react';\n\nfunction useClickOutside(\n ref: RefObject,\n handler: (event: MouseEvent | TouchEvent) => void\n): void {\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent | TouchEvent) => {\n if (!ref || !ref.current || ref.current.contains(event.target as Node)) {\n return;\n }\n\n handler(event);\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n document.addEventListener('touchstart', handleClickOutside);\n\n return () => {\n document.removeEventListener('mousedown', handleClickOutside);\n document.removeEventListener('touchstart', handleClickOutside);\n };\n }, [ref, handler]);\n}\n\nexport default useClickOutside;\n",
8 | "type": "registry:hook"
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/public/mp_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibelick/motion-primitives/ab4563f012c063e542de3babe3b901a8b521a7de/public/mp_dark.png
--------------------------------------------------------------------------------
/public/mp_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibelick/motion-primitives/ab4563f012c063e542de3babe3b901a8b521a7de/public/mp_light.png
--------------------------------------------------------------------------------
/public/sony_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/scripts/registry-hooks.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | export const hooks = [
4 | {
5 | name: 'useClickOutside',
6 | path: path.join(__dirname, '../hooks/useClickOutside.tsx'),
7 | },
8 | {
9 | name: 'usePreventScroll',
10 | path: path.join(__dirname, '../hooks/usePreventScroll.tsx'),
11 | },
12 | ];
13 |
--------------------------------------------------------------------------------
/scripts/registry-schema.ts:
--------------------------------------------------------------------------------
1 | export type RegistryType =
2 | | 'registry:ui'
3 | | 'registry:hook'
4 | | 'registry:block'
5 | | 'registry:component'
6 | | 'registry:lib'
7 | | 'registry:page'
8 | | 'registry:file';
9 |
10 | export interface RegistryFile {
11 | path: string;
12 | content: string;
13 | type: RegistryType;
14 | }
15 |
16 | export interface TailwindConfig {
17 | config?: Record;
18 | }
19 |
20 | export interface CssVars {
21 | light: Record;
22 | dark: Record;
23 | }
24 |
25 | export interface Schema {
26 | name: string;
27 | type: RegistryType;
28 | registryDependencies?: string[];
29 | dependencies?: string[];
30 | devDependencies?: string[];
31 | tailwind?: TailwindConfig;
32 | cssVars?: CssVars;
33 | files: RegistryFile[];
34 | componentName?: string;
35 | description?: string;
36 | }
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------