diff --git a/apps/docs/content/1.component/arrow-tile-button.mdx b/apps/docs/content/1.component/arrow-tile-button.mdx new file mode 100644 index 0000000..f6f53fb --- /dev/null +++ b/apps/docs/content/1.component/arrow-tile-button.mdx @@ -0,0 +1,75 @@ +--- +title: Arrow Tile Button +description: A label slab paired with a detached arrow tile, in a springy tilt and a sliding swap variant. +icon: square-arrow-right +--- + +A label slab sits beside its own **square arrow tile**, and the pair animates +two ways. In `tilt`, the tile **knocks the word**: it leans into the slab, the +slab is shoved back and springs into place, and the letters **topple in a wave** +that travels away from the tile before bouncing upright. In `swap`, the tile **takes the lead**: it rolls across to the front of the +label, its arrow spinning a full turn, while the label slides aside and its +letters **lift in a wake** behind it. Pressing either variant squashes the whole +button. It is a real `\n );\n}\n" + } + ], + "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/arrow-tile-button" +} \ No newline at end of file diff --git a/apps/docs/public/r/focus-frame-button-demo.json b/apps/docs/public/r/focus-frame-button-demo.json new file mode 100644 index 0000000..9c24fe6 --- /dev/null +++ b/apps/docs/public/r/focus-frame-button-demo.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "focus-frame-button-demo", + "type": "registry:example", + "title": "Focus Frame Button Demo", + "description": "Usage examples for Focus Frame Button.", + "registryDependencies": [ + "https://tween-ui.vercel.app/r/focus-frame-button.json" + ], + "files": [ + { + "path": "examples/focus-frame-button-demo.tsx", + "type": "registry:example", + "content": "import FocusFrameButton from '@/components/tweenui/focus-frame-button';\n\nexport default function Page() {\n return (\n console.log('clicked')}>Talk to the team\n );\n}" + } + ] +} \ No newline at end of file diff --git a/apps/docs/public/r/focus-frame-button.json b/apps/docs/public/r/focus-frame-button.json new file mode 100644 index 0000000..cca7496 --- /dev/null +++ b/apps/docs/public/r/focus-frame-button.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "focus-frame-button", + "type": "registry:ui", + "title": "Focus Frame Button", + "description": "A framed button that locks on like a viewfinder on hover: the hairline folds into its corners, the brackets snap shut while the label rolls up and a fresh return arrow slides in.", + "dependencies": [], + "registryDependencies": [], + "cssVars": { + "theme": {} + }, + "files": [ + { + "path": "components/tweenui/focus-frame-button.tsx", + "type": "registry:component", + "target": "components/tweenui/focus-frame-button.tsx", + "content": "import type { ComponentPropsWithRef } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport interface FocusFrameButtonProps extends ComponentPropsWithRef<'button'> {\n children: string;\n}\n\nconst roll =\n 'block transition-transform duration-600 ease-(--expo) group-hover:-translate-y-full group-focus-visible:-translate-y-full motion-reduce:transition-none';\n\nfunction RollLabel({ children }: { children: string }) {\n return (\n \n {children}\n \n {children}\n \n \n );\n}\n\nfunction ReturnArrow({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\nconst slide =\n '-translate-x-full transition-transform duration-600 ease-(--expo) group-hover:translate-x-0 group-focus-visible:translate-x-0 motion-reduce:transition-none';\n\nconst rail =\n 'pointer-events-none absolute bg-[#045f64]/35 transition-transform delay-200 duration-500 ease-(--expo) group-hover:delay-0 group-focus-visible:delay-0 motion-reduce:transition-none dark:bg-[#c6f56f]/35';\n\nconst rails = [\n 'top-0 left-0 h-px w-1/2 origin-left group-hover:scale-x-0 group-focus-visible:scale-x-0',\n 'top-0 right-0 h-px w-1/2 origin-right group-hover:scale-x-0 group-focus-visible:scale-x-0',\n 'bottom-0 left-0 h-px w-1/2 origin-left group-hover:scale-x-0 group-focus-visible:scale-x-0',\n 'right-0 bottom-0 h-px w-1/2 origin-right group-hover:scale-x-0 group-focus-visible:scale-x-0',\n 'top-0 left-0 h-1/2 w-px origin-top group-hover:scale-y-0 group-focus-visible:scale-y-0',\n 'bottom-0 left-0 h-1/2 w-px origin-bottom group-hover:scale-y-0 group-focus-visible:scale-y-0',\n 'top-0 right-0 h-1/2 w-px origin-top group-hover:scale-y-0 group-focus-visible:scale-y-0',\n 'right-0 bottom-0 h-1/2 w-px origin-bottom group-hover:scale-y-0 group-focus-visible:scale-y-0',\n];\n\nconst corner =\n 'pointer-events-none absolute size-2.5 border-[#045f64] transition-transform duration-500 ease-(--expo) group-hover:delay-200 group-hover:duration-700 group-hover:ease-(--snap) group-focus-visible:delay-200 group-focus-visible:duration-700 group-focus-visible:ease-(--snap) motion-reduce:transition-none dark:border-[#c6f56f]';\n\nconst corners = [\n 'top-0 left-0 border-t-[1.5px] border-l-[1.5px] group-hover:translate-1 group-focus-visible:translate-1',\n 'top-0 right-0 border-t-[1.5px] border-r-[1.5px] group-hover:-translate-x-1 group-hover:translate-y-1 group-focus-visible:-translate-x-1 group-focus-visible:translate-y-1',\n 'bottom-0 left-0 border-b-[1.5px] border-l-[1.5px] group-hover:translate-x-1 group-hover:-translate-y-1 group-focus-visible:translate-x-1 group-focus-visible:-translate-y-1',\n 'right-0 bottom-0 border-r-[1.5px] border-b-[1.5px] group-hover:-translate-1 group-focus-visible:-translate-1',\n];\n\nexport default function FocusFrameButton({\n children,\n className,\n type = 'button',\n ...props\n}: FocusFrameButtonProps) {\n return (\n \n {rails.map((placement) => (\n \n ))}\n {corners.map((placement) => (\n \n ))}\n \n {children}\n \n \n \n \n \n \n );\n}\n" + } + ], + "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/focus-frame-button" +} \ No newline at end of file diff --git a/apps/docs/public/r/registry.json b/apps/docs/public/r/registry.json index 931081d..970b914 100644 --- a/apps/docs/public/r/registry.json +++ b/apps/docs/public/r/registry.json @@ -343,6 +343,78 @@ } ] }, + { + "name": "arrow-tile-button", + "type": "registry:ui", + "title": "Arrow Tile Button", + "description": "A label slab paired with a detached arrow tile: the tilt variant has the tile knock the slab so its letters topple in a wave away from it and spring upright, and the swap variant rolls the tile to the front as the letters lift in its wake.", + "files": [ + { + "path": "components/tweenui/arrow-tile-button.tsx", + "type": "registry:component" + } + ] + }, + { + "name": "arrow-tile-button-demo", + "type": "registry:example", + "title": "Arrow Tile Button Demo", + "description": "Usage examples for Arrow Tile Button.", + "files": [ + { + "path": "examples/arrow-tile-button-demo.tsx", + "type": "registry:example" + } + ] + }, + { + "name": "focus-frame-button", + "type": "registry:ui", + "title": "Focus Frame Button", + "description": "A framed button that locks on like a viewfinder on hover: the hairline folds into its corners, the brackets snap shut while the label rolls up and a fresh return arrow slides in.", + "files": [ + { + "path": "components/tweenui/focus-frame-button.tsx", + "type": "registry:component" + } + ] + }, + { + "name": "focus-frame-button-demo", + "type": "registry:example", + "title": "Focus Frame Button Demo", + "description": "Usage examples for Focus Frame Button.", + "files": [ + { + "path": "examples/focus-frame-button-demo.tsx", + "type": "registry:example" + } + ] + }, + { + "name": "slant-morph-button", + "type": "registry:ui", + "title": "Slant Morph Button", + "description": "A slanted label and arrow tile whose diagonal seam smoothly flips its lean like a hinge on hover with GSAP MorphSVG, while the colors trade places and a fresh arrow slides through.", + "files": [ + { + "path": "components/tweenui/slant-morph-button.tsx", + "type": "registry:component" + } + ] + }, + { + "name": "slant-morph-button-demo", + "type": "registry:example", + "title": "Slant Morph Button Demo", + "description": "Usage examples for Slant Morph Button.", + "files": [ + { + "path": "examples/slant-morph-button-demo.tsx", + "type": "registry:example" + } + ] + }, { "name": "logo-orbit", "type": "registry:ui", diff --git a/apps/docs/public/r/slant-morph-button-demo.json b/apps/docs/public/r/slant-morph-button-demo.json new file mode 100644 index 0000000..10a9bd8 --- /dev/null +++ b/apps/docs/public/r/slant-morph-button-demo.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "slant-morph-button-demo", + "type": "registry:example", + "title": "Slant Morph Button Demo", + "description": "Usage examples for Slant Morph Button.", + "registryDependencies": [ + "https://tween-ui.vercel.app/r/slant-morph-button.json" + ], + "files": [ + { + "path": "examples/slant-morph-button-demo.tsx", + "type": "registry:example", + "content": "import SlantMorphButton from '@/components/tweenui/slant-morph-button';\n\nexport default function Page() {\n return (\n console.log('clicked')}>Discover the library\n );\n}" + } + ] +} \ No newline at end of file diff --git a/apps/docs/public/r/slant-morph-button.json b/apps/docs/public/r/slant-morph-button.json new file mode 100644 index 0000000..9614f7d --- /dev/null +++ b/apps/docs/public/r/slant-morph-button.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "slant-morph-button", + "type": "registry:ui", + "title": "Slant Morph Button", + "description": "A slanted label and arrow tile whose diagonal seam smoothly flips its lean like a hinge on hover with GSAP MorphSVG, while the colors trade places and a fresh arrow slides through.", + "dependencies": [ + "gsap", + "@gsap/react" + ], + "registryDependencies": [], + "cssVars": { + "theme": {} + }, + "files": [ + { + "path": "components/tweenui/slant-morph-button.tsx", + "type": "registry:component", + "target": "components/tweenui/slant-morph-button.tsx", + "content": "'use client';\n\nimport { useRef, type ComponentPropsWithRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, MorphSVGPlugin);\n\nconst SEAM_REST =\n 'M0 0H5.63C13.438 0 19.166 7.337 17.272 14.91L11.182 39.269A11.527 11.527 0 0 1 0 48V0Z';\nconst SEAM_FLIPPED =\n 'M0 48H5.63C13.438 48 19.166 40.663 17.272 33.09L11.182 8.731A11.527 11.527 0 0 0 0 0V48Z';\nconst TILE_REST =\n 'M6.728 9.09A12 12 0 0 1 18.369 0H39C45.627 0 51 5.373 51 12V36C51 42.627 45.627 48 39 48H12.37C4.561 48 -1.167 40.663 0.727 33.09L6.728 9.09Z';\nconst TILE_FLIPPED =\n 'M6.728 38.91A12 12 0 0 0 18.369 48H39C45.627 48 51 42.627 51 36V12C51 5.373 45.627 0 39 0H12.37C4.561 0 -1.167 7.337 0.727 14.91L6.728 38.91Z';\nconst ARROW = 'M7.703 5.8H.398V4.6h7.305l-3.36-3.36.855-.84 4.8 4.8-4.8 4.8-.855-.84 3.36-3.36Z';\n\nconst canMorph = () =>\n typeof window !== 'undefined' &&\n !window.matchMedia('(prefers-reduced-motion: reduce)').matches &&\n window.matchMedia('(hover: hover) and (pointer: fine)').matches;\n\nconst swap = 'transition-[fill] duration-600 ease-(--settle) motion-reduce:transition-none';\n\nconst slide =\n 'size-2.5 flex-none -translate-x-full transition-transform duration-600 ease-(--settle) group-hover:translate-x-0 group-focus-visible:translate-x-0 motion-reduce:transition-none';\n\nexport interface SlantMorphButtonProps extends ComponentPropsWithRef<'button'> {\n children: string;\n}\n\nexport default function SlantMorphButton({\n children,\n className,\n type = 'button',\n disabled,\n ref,\n ...props\n}: SlantMorphButtonProps) {\n const btnRef = useRef(null);\n const seamRef = useRef(null);\n const tileRef = useRef(null);\n\n const setRef = (node: HTMLButtonElement | null) => {\n btnRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n };\n\n useGSAP(\n () => {\n const btn = btnRef.current;\n const seam = seamRef.current;\n const tile = tileRef.current;\n if (!btn || !seam || !tile || disabled || !canMorph()) return;\n\n const hinge = gsap\n .timeline({ paused: true, defaults: { duration: 0.6, ease: 'power3.inOut' } })\n .to(seam, { morphSVG: { shape: SEAM_FLIPPED, shapeIndex: 'auto' } }, 0)\n .to(tile, { morphSVG: { shape: TILE_FLIPPED, shapeIndex: 'auto' } }, 0);\n\n const open = () => hinge.play();\n const close = () => hinge.reverse();\n const onFocus = () => {\n if (btn.matches(':focus-visible')) open();\n };\n const onBlur = () => {\n if (!btn.matches(':hover')) close();\n };\n\n btn.addEventListener('pointerenter', open);\n btn.addEventListener('pointerleave', close);\n btn.addEventListener('focus', onFocus);\n btn.addEventListener('blur', onBlur);\n\n return () => {\n hinge.kill();\n btn.removeEventListener('pointerenter', open);\n btn.removeEventListener('pointerleave', close);\n btn.removeEventListener('focus', onFocus);\n btn.removeEventListener('blur', onBlur);\n };\n },\n { scope: btnRef, dependencies: [disabled], revertOnUpdate: true }\n );\n\n return (\n \n \n {children}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" + } + ], + "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/slant-morph-button\nGSAP plugins (MorphSVGPlugin) are registered inside the component, so there is nothing to set up." +} \ No newline at end of file diff --git a/apps/docs/registry/__index__.generated.tsx b/apps/docs/registry/__index__.generated.tsx index f4df9cc..b349798 100644 --- a/apps/docs/registry/__index__.generated.tsx +++ b/apps/docs/registry/__index__.generated.tsx @@ -24,6 +24,9 @@ export const demos: Record = { 'glow-button:default': dynamic(() => import('./demos/glow-button')), 'magnetic-button:default': dynamic(() => import('./demos/magnetic-button')), 'dot-bloom-button:default': dynamic(() => import('./demos/dot-bloom-button')), + 'arrow-tile-button:default': dynamic(() => import('./demos/arrow-tile-button')), + 'focus-frame-button:default': dynamic(() => import('./demos/focus-frame-button')), + 'slant-morph-button:default': dynamic(() => import('./demos/slant-morph-button')), 'logo-orbit:default': dynamic(() => import('./demos/logo-orbit')), 'logo-cycle:default': dynamic(() => import('./demos/logo-cycle')), 'logo-wave:default': dynamic(() => import('./demos/logo-wave')), diff --git a/apps/docs/registry/__media__.generated.ts b/apps/docs/registry/__media__.generated.ts index 4a2587e..8640ec6 100644 --- a/apps/docs/registry/__media__.generated.ts +++ b/apps/docs/registry/__media__.generated.ts @@ -21,6 +21,9 @@ export const discoveredMedia: Record = { 'glow-button': {"image":"/media/components/glow-button-poster.webp","video":"/media/components/glow-button-preview.mp4"}, 'magnetic-button': {"image":"/media/components/magnetic-button-poster.webp","video":"/media/components/magnetic-button-preview.mp4"}, 'dot-bloom-button': {"image":"/media/components/dot-bloom-button-poster.webp","video":"/media/components/dot-bloom-button-preview.mp4"}, + 'arrow-tile-button': {"image":"/media/components/arrow-tile-button-poster.webp","video":"/media/components/arrow-tile-button-preview.mp4"}, + 'focus-frame-button': {"image":"/media/components/focus-frame-button-poster.webp","video":"/media/components/focus-frame-button-preview.mp4"}, + 'slant-morph-button': {"image":"/media/components/slant-morph-button-poster.webp","video":"/media/components/slant-morph-button-preview.mp4"}, 'logo-orbit': {"image":"/media/components/logo-orbit-poster.webp","video":"/media/components/logo-orbit-preview.mp4"}, 'logo-cycle': {"image":"/media/components/logo-cycle-poster.webp","video":"/media/components/logo-cycle-preview.mp4"}, 'logo-wave': {"image":"/media/components/logo-wave-poster.webp","video":"/media/components/logo-wave-preview.mp4"}, diff --git a/apps/docs/registry/__sources__.generated.ts b/apps/docs/registry/__sources__.generated.ts index 2ff4755..712e9cb 100644 --- a/apps/docs/registry/__sources__.generated.ts +++ b/apps/docs/registry/__sources__.generated.ts @@ -91,6 +91,24 @@ export const usage: Record = { "codeHtml": "
import DotBloomButton from '@/components/tweenui/dot-bloom-button';\n\nexport default function Example() {\n  return <DotBloomButton onClick={() => {}}>Get started</DotBloomButton>;\n}
" } }, + "arrow-tile-button": { + "react": { + "code": "import ArrowTileButton from '@/components/tweenui/arrow-tile-button';\n\nexport default function Example() {\n return {}}>Get started;\n}", + "codeHtml": "
import ArrowTileButton from '@/components/tweenui/arrow-tile-button';\n\nexport default function Example() {\n  return <ArrowTileButton onClick={() => {}}>Get started</ArrowTileButton>;\n}
" + } + }, + "focus-frame-button": { + "react": { + "code": "import FocusFrameButton from '@/components/tweenui/focus-frame-button';\n\nexport default function Example() {\n return {}}>Talk to the team;\n}", + "codeHtml": "
import FocusFrameButton from '@/components/tweenui/focus-frame-button';\n\nexport default function Example() {\n  return <FocusFrameButton onClick={() => {}}>Talk to the team</FocusFrameButton>;\n}
" + } + }, + "slant-morph-button": { + "react": { + "code": "import SlantMorphButton from '@/components/tweenui/slant-morph-button';\n\nexport default function Example() {\n return {}}>Discover the library;\n}", + "codeHtml": "
import SlantMorphButton from '@/components/tweenui/slant-morph-button';\n\nexport default function Example() {\n  return <SlantMorphButton onClick={() => {}}>Discover the library</SlantMorphButton>;\n}
" + } + }, "logo-orbit": { "react": { "code": "import LogoOrbit from '@/components/tweenui/logo-orbit';\n\nconst logos = [\n { src: '/logos/slack.svg', alt: 'Slack' },\n { src: '/logos/figma.svg', alt: 'Figma' },\n { src: '/logos/github.svg', alt: 'GitHub' },\n];\n\nexport default function Example() {\n return ;\n}", @@ -388,6 +406,24 @@ export const manual: Record = { "codeHtml": "
import type { ComponentPropsWithRef } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport interface DotBloomButtonProps extends ComponentPropsWithRef<'button'> {\n  children: string;\n  variant?: 'primary' | 'secondary';\n}\n\nconst variants = {\n  primary:\n    'bg-[#045f64] text-[#c6f56f] [--bloom:#c6f56f] hover:text-[#045f64] focus-visible:text-[#045f64]',\n  secondary:\n    'bg-[#c6f56f] text-[#045f64] [--bloom:#045f64] hover:text-[#c6f56f] focus-visible:text-[#c6f56f]',\n};\n\nconst roll =\n  'block transition-transform duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:-translate-y-full group-focus-visible:-translate-y-full motion-reduce:transition-none motion-reduce:group-hover:translate-y-0 motion-reduce:group-focus-visible:translate-y-0';\n\nexport default function DotBloomButton({\n  children,\n  className,\n  variant = 'primary',\n  type = 'button',\n  ...props\n}: DotBloomButtonProps) {\n  return (\n    <button\n      type={type}\n      className={cn(\n        'group relative isolate inline-flex h-11 cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-md px-5 text-sm font-medium tracking-wide whitespace-nowrap uppercase transition-colors duration-400 ease-[cubic-bezier(0.65,0,0.35,1)] focus-visible:ring-2 focus-visible:ring-[#045f64] focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-60 motion-reduce:transition-none dark:ring-offset-[#12161F]',\n        variants[variant],\n        className\n      )}\n      {...props}\n    >\n      <span aria-hidden=\"true\" className=\"size-1.5 flex-none rounded-full bg-current text-left\">\n        <span className=\"pointer-events-none absolute -z-10 aspect-square w-[calc(200%+100px)] -translate-x-[calc(50%-3px)] -translate-y-[calc(50%-3px)] scale-0 rounded-full bg-(--bloom) transition-transform duration-650 ease-[cubic-bezier(0.65,0,0.35,1)] group-hover:scale-100 group-focus-visible:scale-100 motion-reduce:transition-none\" />\n      </span>\n      <span className=\"relative block overflow-hidden leading-5\">\n        <span className={roll}>{children}</span>\n        <span\n          aria-hidden=\"true\"\n          className={cn(roll, 'absolute top-full left-0 motion-reduce:hidden')}\n        >\n          {children}\n        </span>\n      </span>\n    </button>\n  );\n}\n
" } }, + "arrow-tile-button": { + "react": { + "code": "import type { ComponentPropsWithRef, CSSProperties } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport interface ArrowTileButtonProps extends ComponentPropsWithRef<'button'> {\n children: string;\n variant?: 'tilt' | 'swap';\n}\n\nfunction Arrow({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\ninterface Wave {\n leadMs: number;\n spreadMs: number;\n}\n\nfunction waveDelay(index: number, count: number, { leadMs, spreadMs }: Wave): CSSProperties {\n const step = count > 1 ? spreadMs / (count - 1) : 0;\n const style: CSSProperties & { '--hop-delay': string } = {\n '--hop-delay': `${leadMs + Math.round((count - 1 - index) * step)}ms`,\n };\n return style;\n}\n\nconst toppleChar =\n 'inline-block whitespace-pre transition-none group-[:is(:hover,:focus-visible)]:[translate:0_45%] group-[:is(:hover,:focus-visible)]:[rotate:-14deg] group-[:is(:hover,:focus-visible)]:[scale:1_0.35] group-[:is(:hover,:focus-visible)]:[transform:scale(1,2.8571)_rotate(14deg)_translateY(-45%)] group-[:is(:hover,:focus-visible)]:[transition:translate_145ms_var(--dip)_var(--hop-delay),rotate_145ms_var(--dip)_var(--hop-delay),scale_145ms_var(--dip)_var(--hop-delay),transform_580ms_var(--spring)_calc(var(--hop-delay)+145ms)] motion-reduce:transition-none';\n\nconst liftChar =\n 'inline-block whitespace-pre transition-none group-[:is(:hover,:focus-visible)]:[translate:0_-40%] group-[:is(:hover,:focus-visible)]:[rotate:9deg] group-[:is(:hover,:focus-visible)]:[transform:rotate(-9deg)_translateY(40%)] group-[:is(:hover,:focus-visible)]:[transition:translate_160ms_var(--lift)_var(--hop-delay),rotate_160ms_var(--lift)_var(--hop-delay),transform_560ms_var(--spring)_calc(var(--hop-delay)+160ms)] motion-reduce:transition-none';\n\nfunction WaveLabel({\n children,\n charClassName,\n wave,\n}: {\n children: string;\n charClassName: string;\n wave: Wave;\n}) {\n const chars = Array.from(children);\n return (\n <>\n {children}\n \n {chars.map((char, index) => (\n \n {char}\n \n ))}\n \n \n );\n}\n\nconst TOPPLE_WAVE: Wave = { leadMs: 140, spreadMs: 225 };\nconst LIFT_WAVE: Wave = { leadMs: 40, spreadMs: 340 };\n\nconst knockRelease =\n 'grid [transition:translate_150ms_var(--soft),rotate_150ms_var(--soft),scale_150ms_var(--soft),transform_150ms_var(--soft)] motion-reduce:transition-none';\n\nfunction TiltContent({ children }: { children: string }) {\n return (\n <>\n \n \n \n \n {children}\n \n \n \n \n \n \n \n \n \n \n );\n}\n\nfunction SwapContent({ children }: { children: string }) {\n return (\n <>\n \n \n {children}\n \n \n \n \n
\n \n );\n}\n\nconst variants = {\n tilt: 'text-[#c6f56f] [--bounce:cubic-bezier(0.34,2.27,0.64,1)] [--dip:cubic-bezier(0.55,0.085,0.68,0.53)] [--soft:cubic-bezier(0.59,1,0.88,1.01)]',\n swap: 'pr-[2.875rem] [--circ:cubic-bezier(0.785,0.135,0.15,0.86)] [--lift:cubic-bezier(0.33,1,0.68,1)]',\n};\n\nexport default function ArrowTileButton({\n children,\n className,\n variant = 'tilt',\n type = 'button',\n ...props\n}: ArrowTileButtonProps) {\n return (\n \n {variant === 'swap' ? (\n {children}\n ) : (\n {children}\n )}\n \n );\n}\n", + "codeHtml": "
import type { ComponentPropsWithRef, CSSProperties } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport interface ArrowTileButtonProps extends ComponentPropsWithRef<'button'> {\n  children: string;\n  variant?: 'tilt' | 'swap';\n}\n\nfunction Arrow({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.75}\n      strokeLinecap=\"square\"\n      aria-hidden=\"true\"\n      className={cn('size-4 flex-none', className)}\n    >\n      <path d=\"M2.5 8h10M8.5 3.5 13 8l-4.5 4.5\" />\n    </svg>\n  );\n}\n\ninterface Wave {\n  leadMs: number;\n  spreadMs: number;\n}\n\nfunction waveDelay(index: number, count: number, { leadMs, spreadMs }: Wave): CSSProperties {\n  const step = count > 1 ? spreadMs / (count - 1) : 0;\n  const style: CSSProperties & { '--hop-delay': string } = {\n    '--hop-delay': `${leadMs + Math.round((count - 1 - index) * step)}ms`,\n  };\n  return style;\n}\n\nconst toppleChar =\n  'inline-block whitespace-pre transition-none group-[:is(:hover,:focus-visible)]:[translate:0_45%] group-[:is(:hover,:focus-visible)]:[rotate:-14deg] group-[:is(:hover,:focus-visible)]:[scale:1_0.35] group-[:is(:hover,:focus-visible)]:[transform:scale(1,2.8571)_rotate(14deg)_translateY(-45%)] group-[:is(:hover,:focus-visible)]:[transition:translate_145ms_var(--dip)_var(--hop-delay),rotate_145ms_var(--dip)_var(--hop-delay),scale_145ms_var(--dip)_var(--hop-delay),transform_580ms_var(--spring)_calc(var(--hop-delay)+145ms)] motion-reduce:transition-none';\n\nconst liftChar =\n  'inline-block whitespace-pre transition-none group-[:is(:hover,:focus-visible)]:[translate:0_-40%] group-[:is(:hover,:focus-visible)]:[rotate:9deg] group-[:is(:hover,:focus-visible)]:[transform:rotate(-9deg)_translateY(40%)] group-[:is(:hover,:focus-visible)]:[transition:translate_160ms_var(--lift)_var(--hop-delay),rotate_160ms_var(--lift)_var(--hop-delay),transform_560ms_var(--spring)_calc(var(--hop-delay)+160ms)] motion-reduce:transition-none';\n\nfunction WaveLabel({\n  children,\n  charClassName,\n  wave,\n}: {\n  children: string;\n  charClassName: string;\n  wave: Wave;\n}) {\n  const chars = Array.from(children);\n  return (\n    <>\n      <span className=\"sr-only\">{children}</span>\n      <span aria-hidden=\"true\">\n        {chars.map((char, index) => (\n          <span key={index} className={charClassName} style={waveDelay(index, chars.length, wave)}>\n            {char}\n          </span>\n        ))}\n      </span>\n    </>\n  );\n}\n\nconst TOPPLE_WAVE: Wave = { leadMs: 140, spreadMs: 225 };\nconst LIFT_WAVE: Wave = { leadMs: 40, spreadMs: 340 };\n\nconst knockRelease =\n  'grid [transition:translate_150ms_var(--soft),rotate_150ms_var(--soft),scale_150ms_var(--soft),transform_150ms_var(--soft)] motion-reduce:transition-none';\n\nfunction TiltContent({ children }: { children: string }) {\n  return (\n    <>\n      <span\n        className={cn(\n          knockRelease,\n          'origin-left group-[:is(:hover,:focus-visible)]:[translate:-0.3em_0] group-[:is(:hover,:focus-visible)]:[scale:0.95_1.04] group-[:is(:hover,:focus-visible)]:[transform:scale(1.0526,0.9615)_translateX(0.3em)] group-[:is(:hover,:focus-visible)]:[transition:translate_110ms_var(--dip)_60ms,scale_110ms_var(--dip)_60ms,transform_420ms_170ms_var(--bounce)]'\n        )}\n      >\n        <span aria-hidden=\"true\" className=\"col-start-1 row-start-1 rounded-md bg-[#045f64]\" />\n        <span className=\"relative col-start-1 row-start-1 flex items-center px-5\">\n          <WaveLabel charClassName={toppleChar} wave={TOPPLE_WAVE}>\n            {children}\n          </WaveLabel>\n        </span>\n      </span>\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          knockRelease,\n          '-ml-px size-11 origin-bottom-left group-[:is(:hover,:focus-visible)]:[translate:-0.2em_0] group-[:is(:hover,:focus-visible)]:[rotate:-12deg] group-[:is(:hover,:focus-visible)]:[transform:rotate(12deg)_translateX(0.2em)] group-[:is(:hover,:focus-visible)]:[transition:translate_120ms_var(--dip),rotate_120ms_var(--dip),transform_400ms_120ms_var(--bounce)]'\n        )}\n      >\n        <span className=\"col-start-1 row-start-1 rounded-md bg-[#045f64]\" />\n        <span className=\"relative col-start-1 row-start-1 grid place-items-center\">\n          <Arrow />\n        </span>\n      </span>\n    </>\n  );\n}\n\nfunction SwapContent({ children }: { children: string }) {\n  return (\n    <>\n      <span className=\"flex items-center rounded-md bg-[#c6f56f] px-5 text-[#045f64] transition-[translate] duration-500 ease-(--circ) group-[:is(:hover,:focus-visible)]:translate-x-[2.875rem] motion-reduce:transition-none\">\n        <WaveLabel charClassName={liftChar} wave={LIFT_WAVE}>\n          {children}\n        </WaveLabel>\n      </span>\n      <span\n        aria-hidden=\"true\"\n        className=\"absolute top-0 right-0 z-10 grid size-11 place-items-center rounded-md bg-[#045f64] text-[#c6f56f] transition-[right] duration-500 ease-(--circ) group-[:is(:hover,:focus-visible)]:right-[calc(100%-2.75rem)] motion-reduce:transition-none\"\n      >\n        <Arrow className=\"transition-[rotate] duration-500 ease-(--circ) group-[:is(:hover,:focus-visible)]:-rotate-360 motion-reduce:transition-none\" />\n      </span>\n    </>\n  );\n}\n\nconst variants = {\n  tilt: 'text-[#c6f56f] [--bounce:cubic-bezier(0.34,2.27,0.64,1)] [--dip:cubic-bezier(0.55,0.085,0.68,0.53)] [--soft:cubic-bezier(0.59,1,0.88,1.01)]',\n  swap: 'pr-[2.875rem] [--circ:cubic-bezier(0.785,0.135,0.15,0.86)] [--lift:cubic-bezier(0.33,1,0.68,1)]',\n};\n\nexport default function ArrowTileButton({\n  children,\n  className,\n  variant = 'tilt',\n  type = 'button',\n  ...props\n}: ArrowTileButtonProps) {\n  return (\n    <button\n      type={type}\n      className={cn(\n        'group relative inline-flex h-11 cursor-pointer items-stretch text-sm font-medium tracking-wide whitespace-nowrap uppercase transition-[scale] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)] select-none [--spring:linear(0,0.406_4.2%,0.855_8.3%,1.161_12.5%,1.273_16.7%,1.234_20.8%,1.125_25%,1.017_29.2%,0.95_33.3%,0.931_37.5%,0.946_41.7%,0.975_45.8%,1_50%,1.014_54.2%,1.017_58.3%,1.012_62.5%,1.005_66.7%,0.999_70.8%,0.996_75%,0.997_83.3%,1_91.7%,1)] focus-visible:ring-2 focus-visible:ring-[#045f64] focus-visible:ring-offset-2 focus-visible:outline-none active:[scale:0.955_0.925] disabled:pointer-events-none disabled:opacity-60 motion-reduce:transition-none dark:ring-offset-[#12161F]',\n        variants[variant],\n        className\n      )}\n      {...props}\n    >\n      {variant === 'swap' ? (\n        <SwapContent>{children}</SwapContent>\n      ) : (\n        <TiltContent>{children}</TiltContent>\n      )}\n    </button>\n  );\n}\n
" + } + }, + "focus-frame-button": { + "react": { + "code": "import type { ComponentPropsWithRef } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport interface FocusFrameButtonProps extends ComponentPropsWithRef<'button'> {\n children: string;\n}\n\nconst roll =\n 'block transition-transform duration-600 ease-(--expo) group-hover:-translate-y-full group-focus-visible:-translate-y-full motion-reduce:transition-none';\n\nfunction RollLabel({ children }: { children: string }) {\n return (\n \n {children}\n \n {children}\n \n \n );\n}\n\nfunction ReturnArrow({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\nconst slide =\n '-translate-x-full transition-transform duration-600 ease-(--expo) group-hover:translate-x-0 group-focus-visible:translate-x-0 motion-reduce:transition-none';\n\nconst rail =\n 'pointer-events-none absolute bg-[#045f64]/35 transition-transform delay-200 duration-500 ease-(--expo) group-hover:delay-0 group-focus-visible:delay-0 motion-reduce:transition-none dark:bg-[#c6f56f]/35';\n\nconst rails = [\n 'top-0 left-0 h-px w-1/2 origin-left group-hover:scale-x-0 group-focus-visible:scale-x-0',\n 'top-0 right-0 h-px w-1/2 origin-right group-hover:scale-x-0 group-focus-visible:scale-x-0',\n 'bottom-0 left-0 h-px w-1/2 origin-left group-hover:scale-x-0 group-focus-visible:scale-x-0',\n 'right-0 bottom-0 h-px w-1/2 origin-right group-hover:scale-x-0 group-focus-visible:scale-x-0',\n 'top-0 left-0 h-1/2 w-px origin-top group-hover:scale-y-0 group-focus-visible:scale-y-0',\n 'bottom-0 left-0 h-1/2 w-px origin-bottom group-hover:scale-y-0 group-focus-visible:scale-y-0',\n 'top-0 right-0 h-1/2 w-px origin-top group-hover:scale-y-0 group-focus-visible:scale-y-0',\n 'right-0 bottom-0 h-1/2 w-px origin-bottom group-hover:scale-y-0 group-focus-visible:scale-y-0',\n];\n\nconst corner =\n 'pointer-events-none absolute size-2.5 border-[#045f64] transition-transform duration-500 ease-(--expo) group-hover:delay-200 group-hover:duration-700 group-hover:ease-(--snap) group-focus-visible:delay-200 group-focus-visible:duration-700 group-focus-visible:ease-(--snap) motion-reduce:transition-none dark:border-[#c6f56f]';\n\nconst corners = [\n 'top-0 left-0 border-t-[1.5px] border-l-[1.5px] group-hover:translate-1 group-focus-visible:translate-1',\n 'top-0 right-0 border-t-[1.5px] border-r-[1.5px] group-hover:-translate-x-1 group-hover:translate-y-1 group-focus-visible:-translate-x-1 group-focus-visible:translate-y-1',\n 'bottom-0 left-0 border-b-[1.5px] border-l-[1.5px] group-hover:translate-x-1 group-hover:-translate-y-1 group-focus-visible:translate-x-1 group-focus-visible:-translate-y-1',\n 'right-0 bottom-0 border-r-[1.5px] border-b-[1.5px] group-hover:-translate-1 group-focus-visible:-translate-1',\n];\n\nexport default function FocusFrameButton({\n children,\n className,\n type = 'button',\n ...props\n}: FocusFrameButtonProps) {\n return (\n \n {rails.map((placement) => (\n \n ))}\n {corners.map((placement) => (\n \n ))}\n \n {children}\n \n \n \n \n \n \n );\n}\n", + "codeHtml": "
import type { ComponentPropsWithRef } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport interface FocusFrameButtonProps extends ComponentPropsWithRef<'button'> {\n  children: string;\n}\n\nconst roll =\n  'block transition-transform duration-600 ease-(--expo) group-hover:-translate-y-full group-focus-visible:-translate-y-full motion-reduce:transition-none';\n\nfunction RollLabel({ children }: { children: string }) {\n  return (\n    <span className=\"relative block h-5 overflow-hidden leading-5\">\n      <span className={roll}>{children}</span>\n      <span aria-hidden=\"true\" className={roll}>\n        {children}\n      </span>\n    </span>\n  );\n}\n\nfunction ReturnArrow({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 20 20\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.6}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n      className={cn('size-4 flex-none', className)}\n    >\n      <path d=\"M3 3.5v4.57a4 4 0 0 0 4 4h10m-4.43-4.43L17 12.07l-4.43 4.43\" />\n    </svg>\n  );\n}\n\nconst slide =\n  '-translate-x-full transition-transform duration-600 ease-(--expo) group-hover:translate-x-0 group-focus-visible:translate-x-0 motion-reduce:transition-none';\n\nconst rail =\n  'pointer-events-none absolute bg-[#045f64]/35 transition-transform delay-200 duration-500 ease-(--expo) group-hover:delay-0 group-focus-visible:delay-0 motion-reduce:transition-none dark:bg-[#c6f56f]/35';\n\nconst rails = [\n  'top-0 left-0 h-px w-1/2 origin-left group-hover:scale-x-0 group-focus-visible:scale-x-0',\n  'top-0 right-0 h-px w-1/2 origin-right group-hover:scale-x-0 group-focus-visible:scale-x-0',\n  'bottom-0 left-0 h-px w-1/2 origin-left group-hover:scale-x-0 group-focus-visible:scale-x-0',\n  'right-0 bottom-0 h-px w-1/2 origin-right group-hover:scale-x-0 group-focus-visible:scale-x-0',\n  'top-0 left-0 h-1/2 w-px origin-top group-hover:scale-y-0 group-focus-visible:scale-y-0',\n  'bottom-0 left-0 h-1/2 w-px origin-bottom group-hover:scale-y-0 group-focus-visible:scale-y-0',\n  'top-0 right-0 h-1/2 w-px origin-top group-hover:scale-y-0 group-focus-visible:scale-y-0',\n  'right-0 bottom-0 h-1/2 w-px origin-bottom group-hover:scale-y-0 group-focus-visible:scale-y-0',\n];\n\nconst corner =\n  'pointer-events-none absolute size-2.5 border-[#045f64] transition-transform duration-500 ease-(--expo) group-hover:delay-200 group-hover:duration-700 group-hover:ease-(--snap) group-focus-visible:delay-200 group-focus-visible:duration-700 group-focus-visible:ease-(--snap) motion-reduce:transition-none dark:border-[#c6f56f]';\n\nconst corners = [\n  'top-0 left-0 border-t-[1.5px] border-l-[1.5px] group-hover:translate-1 group-focus-visible:translate-1',\n  'top-0 right-0 border-t-[1.5px] border-r-[1.5px] group-hover:-translate-x-1 group-hover:translate-y-1 group-focus-visible:-translate-x-1 group-focus-visible:translate-y-1',\n  'bottom-0 left-0 border-b-[1.5px] border-l-[1.5px] group-hover:translate-x-1 group-hover:-translate-y-1 group-focus-visible:translate-x-1 group-focus-visible:-translate-y-1',\n  'right-0 bottom-0 border-r-[1.5px] border-b-[1.5px] group-hover:-translate-1 group-focus-visible:-translate-1',\n];\n\nexport default function FocusFrameButton({\n  children,\n  className,\n  type = 'button',\n  ...props\n}: FocusFrameButtonProps) {\n  return (\n    <button\n      type={type}\n      className={cn(\n        'group relative inline-flex cursor-pointer p-1 text-sm font-medium tracking-wide whitespace-nowrap text-[#045f64] uppercase select-none [--expo:cubic-bezier(0.16,1,0.3,1)] [--snap:cubic-bezier(0.34,1.56,0.64,1)] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-60',\n        className\n      )}\n      {...props}\n    >\n      {rails.map((placement) => (\n        <span key={placement} aria-hidden=\"true\" className={cn(rail, placement)} />\n      ))}\n      {corners.map((placement) => (\n        <span key={placement} aria-hidden=\"true\" className={cn(corner, placement)} />\n      ))}\n      <span className=\"relative flex h-11 items-center gap-2.5 overflow-hidden rounded-[2px] bg-[#c6f56f] px-5 transition-[scale] duration-200 ease-out group-active:scale-[0.97] motion-reduce:transition-none\">\n        <RollLabel>{children}</RollLabel>\n        <span aria-hidden=\"true\" className=\"relative flex size-4 overflow-hidden\">\n          <ReturnArrow className={slide} />\n          <ReturnArrow className={slide} />\n        </span>\n      </span>\n    </button>\n  );\n}\n
" + } + }, + "slant-morph-button": { + "react": { + "code": "'use client';\n\nimport { useRef, type ComponentPropsWithRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, MorphSVGPlugin);\n\nconst SEAM_REST =\n 'M0 0H5.63C13.438 0 19.166 7.337 17.272 14.91L11.182 39.269A11.527 11.527 0 0 1 0 48V0Z';\nconst SEAM_FLIPPED =\n 'M0 48H5.63C13.438 48 19.166 40.663 17.272 33.09L11.182 8.731A11.527 11.527 0 0 0 0 0V48Z';\nconst TILE_REST =\n 'M6.728 9.09A12 12 0 0 1 18.369 0H39C45.627 0 51 5.373 51 12V36C51 42.627 45.627 48 39 48H12.37C4.561 48 -1.167 40.663 0.727 33.09L6.728 9.09Z';\nconst TILE_FLIPPED =\n 'M6.728 38.91A12 12 0 0 0 18.369 48H39C45.627 48 51 42.627 51 36V12C51 5.373 45.627 0 39 0H12.37C4.561 0 -1.167 7.337 0.727 14.91L6.728 38.91Z';\nconst ARROW = 'M7.703 5.8H.398V4.6h7.305l-3.36-3.36.855-.84 4.8 4.8-4.8 4.8-.855-.84 3.36-3.36Z';\n\nconst canMorph = () =>\n typeof window !== 'undefined' &&\n !window.matchMedia('(prefers-reduced-motion: reduce)').matches &&\n window.matchMedia('(hover: hover) and (pointer: fine)').matches;\n\nconst swap = 'transition-[fill] duration-600 ease-(--settle) motion-reduce:transition-none';\n\nconst slide =\n 'size-2.5 flex-none -translate-x-full transition-transform duration-600 ease-(--settle) group-hover:translate-x-0 group-focus-visible:translate-x-0 motion-reduce:transition-none';\n\nexport interface SlantMorphButtonProps extends ComponentPropsWithRef<'button'> {\n children: string;\n}\n\nexport default function SlantMorphButton({\n children,\n className,\n type = 'button',\n disabled,\n ref,\n ...props\n}: SlantMorphButtonProps) {\n const btnRef = useRef(null);\n const seamRef = useRef(null);\n const tileRef = useRef(null);\n\n const setRef = (node: HTMLButtonElement | null) => {\n btnRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n };\n\n useGSAP(\n () => {\n const btn = btnRef.current;\n const seam = seamRef.current;\n const tile = tileRef.current;\n if (!btn || !seam || !tile || disabled || !canMorph()) return;\n\n const hinge = gsap\n .timeline({ paused: true, defaults: { duration: 0.6, ease: 'power3.inOut' } })\n .to(seam, { morphSVG: { shape: SEAM_FLIPPED, shapeIndex: 'auto' } }, 0)\n .to(tile, { morphSVG: { shape: TILE_FLIPPED, shapeIndex: 'auto' } }, 0);\n\n const open = () => hinge.play();\n const close = () => hinge.reverse();\n const onFocus = () => {\n if (btn.matches(':focus-visible')) open();\n };\n const onBlur = () => {\n if (!btn.matches(':hover')) close();\n };\n\n btn.addEventListener('pointerenter', open);\n btn.addEventListener('pointerleave', close);\n btn.addEventListener('focus', onFocus);\n btn.addEventListener('blur', onBlur);\n\n return () => {\n hinge.kill();\n btn.removeEventListener('pointerenter', open);\n btn.removeEventListener('pointerleave', close);\n btn.removeEventListener('focus', onFocus);\n btn.removeEventListener('blur', onBlur);\n };\n },\n { scope: btnRef, dependencies: [disabled], revertOnUpdate: true }\n );\n\n return (\n \n \n {children}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n", + "codeHtml": "
'use client';\n\nimport { useRef, type ComponentPropsWithRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, MorphSVGPlugin);\n\nconst SEAM_REST =\n  'M0 0H5.63C13.438 0 19.166 7.337 17.272 14.91L11.182 39.269A11.527 11.527 0 0 1 0 48V0Z';\nconst SEAM_FLIPPED =\n  'M0 48H5.63C13.438 48 19.166 40.663 17.272 33.09L11.182 8.731A11.527 11.527 0 0 0 0 0V48Z';\nconst TILE_REST =\n  'M6.728 9.09A12 12 0 0 1 18.369 0H39C45.627 0 51 5.373 51 12V36C51 42.627 45.627 48 39 48H12.37C4.561 48 -1.167 40.663 0.727 33.09L6.728 9.09Z';\nconst TILE_FLIPPED =\n  'M6.728 38.91A12 12 0 0 0 18.369 48H39C45.627 48 51 42.627 51 36V12C51 5.373 45.627 0 39 0H12.37C4.561 0 -1.167 7.337 0.727 14.91L6.728 38.91Z';\nconst ARROW = 'M7.703 5.8H.398V4.6h7.305l-3.36-3.36.855-.84 4.8 4.8-4.8 4.8-.855-.84 3.36-3.36Z';\n\nconst canMorph = () =>\n  typeof window !== 'undefined' &&\n  !window.matchMedia('(prefers-reduced-motion: reduce)').matches &&\n  window.matchMedia('(hover: hover) and (pointer: fine)').matches;\n\nconst swap = 'transition-[fill] duration-600 ease-(--settle) motion-reduce:transition-none';\n\nconst slide =\n  'size-2.5 flex-none -translate-x-full transition-transform duration-600 ease-(--settle) group-hover:translate-x-0 group-focus-visible:translate-x-0 motion-reduce:transition-none';\n\nexport interface SlantMorphButtonProps extends ComponentPropsWithRef<'button'> {\n  children: string;\n}\n\nexport default function SlantMorphButton({\n  children,\n  className,\n  type = 'button',\n  disabled,\n  ref,\n  ...props\n}: SlantMorphButtonProps) {\n  const btnRef = useRef<HTMLButtonElement>(null);\n  const seamRef = useRef<SVGPathElement>(null);\n  const tileRef = useRef<SVGPathElement>(null);\n\n  const setRef = (node: HTMLButtonElement | null) => {\n    btnRef.current = node;\n    if (typeof ref === 'function') ref(node);\n    else if (ref) ref.current = node;\n  };\n\n  useGSAP(\n    () => {\n      const btn = btnRef.current;\n      const seam = seamRef.current;\n      const tile = tileRef.current;\n      if (!btn || !seam || !tile || disabled || !canMorph()) return;\n\n      const hinge = gsap\n        .timeline({ paused: true, defaults: { duration: 0.6, ease: 'power3.inOut' } })\n        .to(seam, { morphSVG: { shape: SEAM_FLIPPED, shapeIndex: 'auto' } }, 0)\n        .to(tile, { morphSVG: { shape: TILE_FLIPPED, shapeIndex: 'auto' } }, 0);\n\n      const open = () => hinge.play();\n      const close = () => hinge.reverse();\n      const onFocus = () => {\n        if (btn.matches(':focus-visible')) open();\n      };\n      const onBlur = () => {\n        if (!btn.matches(':hover')) close();\n      };\n\n      btn.addEventListener('pointerenter', open);\n      btn.addEventListener('pointerleave', close);\n      btn.addEventListener('focus', onFocus);\n      btn.addEventListener('blur', onBlur);\n\n      return () => {\n        hinge.kill();\n        btn.removeEventListener('pointerenter', open);\n        btn.removeEventListener('pointerleave', close);\n        btn.removeEventListener('focus', onFocus);\n        btn.removeEventListener('blur', onBlur);\n      };\n    },\n    { scope: btnRef, dependencies: [disabled], revertOnUpdate: true }\n  );\n\n  return (\n    <button\n      ref={setRef}\n      type={type}\n      disabled={disabled}\n      className={cn(\n        'group relative inline-flex h-12 cursor-pointer items-stretch font-mono text-sm whitespace-nowrap uppercase select-none [--settle:cubic-bezier(0.25,1,0.5,1)] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-60',\n        className\n      )}\n      {...props}\n    >\n      <span className=\"relative mr-4 flex items-center rounded-l-xl bg-[#045f64] py-2 pr-2 pl-5 text-[#c6f56f] transition-colors duration-600 ease-(--settle) group-hover:bg-[#c6f56f] group-hover:text-[#045f64] group-focus-visible:bg-[#c6f56f] group-focus-visible:text-[#045f64] motion-reduce:transition-none\">\n        {children}\n        <svg\n          viewBox=\"0 0 18 48\"\n          aria-hidden=\"true\"\n          className=\"absolute top-0 -right-4 h-full w-[18px] overflow-visible\"\n        >\n          <path\n            ref={seamRef}\n            d={SEAM_REST}\n            className={cn(\n              swap,\n              'fill-[#045f64] group-hover:fill-[#c6f56f] group-focus-visible:fill-[#c6f56f]'\n            )}\n          />\n        </svg>\n      </span>\n      <span aria-hidden=\"true\" className=\"relative grid w-[51px] place-items-center\">\n        <svg viewBox=\"0 0 51 48\" className=\"absolute inset-0 size-full overflow-visible\">\n          <path\n            ref={tileRef}\n            d={TILE_REST}\n            className={cn(\n              swap,\n              'fill-[#c6f56f] group-hover:fill-[#045f64] group-focus-visible:fill-[#045f64]'\n            )}\n          />\n        </svg>\n        <span className=\"relative ml-px flex w-2.5 overflow-hidden\">\n          <svg viewBox=\"0 0 10 10\" className={cn(slide, 'fill-[#c6f56f]')}>\n            <path d={ARROW} />\n          </svg>\n          <svg viewBox=\"0 0 10 10\" className={cn(slide, 'fill-[#045f64]')}>\n            <path d={ARROW} />\n          </svg>\n        </span>\n      </span>\n    </button>\n  );\n}\n
" + } + }, "logo-orbit": { "react": { "code": "'use client';\n\nimport { useRef, type ComponentPropsWithoutRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nfunction LogoImage({ src, srcDark, alt }: LogoOrbitLogo) {\n if (!srcDark) {\n return {alt};\n }\n return (\n <>\n {alt}\n \n \n );\n}\n\nexport interface LogoOrbitLogo {\n src: string;\n /** Optional mark shown under `.dark`. */\n srcDark?: string;\n alt: string;\n}\n\nexport interface LogoOrbitProps extends ComponentPropsWithoutRef<'div'> {\n /** Logos placed evenly around the ring. */\n logos: LogoOrbitLogo[];\n /** Ring diameter in pixels. */\n size?: number;\n /** Rotation speed multiplier (1 = one turn every 20s). */\n speed?: number;\n /** Pause the orbit while the pointer is over the ring. */\n pauseOnHover?: boolean;\n}\n\nexport default function LogoOrbit({\n logos,\n size = 320,\n speed = 1,\n pauseOnHover = true,\n className,\n ...props\n}: LogoOrbitProps) {\n const ringRef = useRef(null);\n const hubRef = useRef(null);\n const pivotRefs = useRef<(HTMLDivElement | null)[]>([]);\n const itemRefs = useRef<(HTMLDivElement | null)[]>([]);\n\n useGSAP(\n () => {\n const ring = ringRef.current;\n const hub = hubRef.current;\n if (!ring || !hub) return;\n\n ring.style.setProperty('--orbit-size', `${size}px`);\n\n const hubSize = hub.offsetWidth || 20;\n const hubOffset = (size - hubSize) / 2;\n gsap.set(hub, { x: hubOffset, y: hubOffset, rotation: 0 });\n\n const pivots = pivotRefs.current.filter((el): el is HTMLDivElement => el !== null);\n const items = itemRefs.current.filter((el): el is HTMLDivElement => el !== null);\n const count = pivots.length;\n if (count === 0) return;\n\n // Distribute the logos evenly around the ring and keep each one upright.\n const space = 360 / count;\n const pivotOriginY = size / 2 + 10;\n pivots.forEach((pivot, i) => {\n const pivotHalf = pivot.offsetWidth / 2 || 10;\n gsap.set(pivot, {\n rotation: i * space,\n transformOrigin: `${pivotHalf}px ${pivotOriginY}px`,\n });\n });\n items.forEach((item, i) => {\n gsap.set(item, { rotation: -i * space, transformOrigin: 'center center' });\n });\n\n if (prefersReducedMotion()) return;\n\n const timeScale = Math.min(20, Math.max(0.05, speed));\n const tl = gsap.timeline({ repeat: -1 });\n tl.to(hub, { rotation: 360, duration: 20, ease: 'none' });\n tl.to(items, { rotation: '-=360', duration: 20, ease: 'none' }, 0);\n tl.timeScale(timeScale);\n\n if (!pauseOnHover) return;\n\n const pause = () => tl.pause();\n const resume = () => tl.resume();\n ring.addEventListener('pointerenter', pause);\n ring.addEventListener('pointerleave', resume);\n return () => {\n ring.removeEventListener('pointerenter', pause);\n ring.removeEventListener('pointerleave', resume);\n };\n },\n { scope: ringRef, dependencies: [size, speed, logos.length, pauseOnHover] }\n );\n\n return (\n \n
\n {logos.map((logo, i) => (\n {\n pivotRefs.current[i] = el;\n }}\n data-orbit-pivot\n className=\"absolute top-[calc(var(--orbit-size)*-0.5)]\"\n >\n {\n itemRefs.current[i] = el;\n }}\n className=\"absolute top-[-20px] grid size-16 place-items-center overflow-hidden rounded-full border border-[#045f64]/10 bg-white p-3 shadow-[0_1px_2px_rgba(16,24,40,0.12)] dark:border-[#045f64]/40 dark:bg-[#12161F]\"\n >\n \n
\n \n ))}\n \n \n );\n}\n", diff --git a/apps/docs/registry/demos/arrow-tile-button.tsx b/apps/docs/registry/demos/arrow-tile-button.tsx new file mode 100644 index 0000000..8d482d9 --- /dev/null +++ b/apps/docs/registry/demos/arrow-tile-button.tsx @@ -0,0 +1,10 @@ +import ArrowTileButton from '@/registry/tweenui/arrow-tile-button'; + +export default function ArrowTileButtonDemo() { + return ( +
+ Get started + Book a call +
+ ); +} diff --git a/apps/docs/registry/demos/focus-frame-button.tsx b/apps/docs/registry/demos/focus-frame-button.tsx new file mode 100644 index 0000000..f444abf --- /dev/null +++ b/apps/docs/registry/demos/focus-frame-button.tsx @@ -0,0 +1,5 @@ +import FocusFrameButton from '@/registry/tweenui/focus-frame-button'; + +export default function FocusFrameButtonDemo() { + return Talk to the team; +} diff --git a/apps/docs/registry/demos/slant-morph-button.tsx b/apps/docs/registry/demos/slant-morph-button.tsx new file mode 100644 index 0000000..7dd5371 --- /dev/null +++ b/apps/docs/registry/demos/slant-morph-button.tsx @@ -0,0 +1,7 @@ +'use client'; + +import SlantMorphButton from '@/registry/tweenui/slant-morph-button'; + +export default function SlantMorphButtonDemo() { + return Discover the library; +} diff --git a/apps/docs/registry/registry-ui.ts b/apps/docs/registry/registry-ui.ts index a2a9f7a..35aa353 100644 --- a/apps/docs/registry/registry-ui.ts +++ b/apps/docs/registry/registry-ui.ts @@ -521,6 +521,105 @@ export default function Example() { export default function Example() { return {}}>Get started; +}`, + }, + }, + { + name: 'arrow-tile-button', + type: 'component', + addedAt: '2026-10-08', + title: 'Arrow Tile Button', + description: + 'A label slab paired with a detached arrow tile: the tilt variant has the tile knock the slab so its letters topple in a wave away from it and spring upright, and the swap variant rolls the tile to the front as the letters lift in its wake.', + media: {}, + dependencies: [], + registryDependencies: [], + cssVars: {}, + files: [ + { + path: 'registry/tweenui/arrow-tile-button.tsx', + target: 'components/tweenui/arrow-tile-button.tsx', + kind: 'react', + }, + ], + variants: [ + { + id: 'default', + label: 'Preview', + reactSource: 'registry/tweenui/arrow-tile-button.tsx', + }, + ], + usage: { + react: `import ArrowTileButton from '@/components/tweenui/arrow-tile-button'; + +export default function Example() { + return {}}>Get started; +}`, + }, + }, + { + name: 'focus-frame-button', + type: 'component', + addedAt: '2026-10-08', + title: 'Focus Frame Button', + description: + 'A framed button that locks on like a viewfinder on hover: the hairline folds into its corners, the brackets snap shut while the label rolls up and a fresh return arrow slides in.', + media: {}, + dependencies: [], + registryDependencies: [], + cssVars: {}, + files: [ + { + path: 'registry/tweenui/focus-frame-button.tsx', + target: 'components/tweenui/focus-frame-button.tsx', + kind: 'react', + }, + ], + variants: [ + { + id: 'default', + label: 'Preview', + reactSource: 'registry/tweenui/focus-frame-button.tsx', + }, + ], + usage: { + react: `import FocusFrameButton from '@/components/tweenui/focus-frame-button'; + +export default function Example() { + return {}}>Talk to the team; +}`, + }, + }, + { + name: 'slant-morph-button', + type: 'component', + addedAt: '2026-10-08', + title: 'Slant Morph Button', + description: + 'A slanted label and arrow tile whose diagonal seam smoothly flips its lean like a hinge on hover with GSAP MorphSVG, while the colors trade places and a fresh arrow slides through.', + media: {}, + dependencies: ['gsap', '@gsap/react'], + registryDependencies: [], + cssVars: {}, + files: [ + { + path: 'registry/tweenui/slant-morph-button.tsx', + target: 'components/tweenui/slant-morph-button.tsx', + kind: 'react', + }, + ], + variants: [ + { + id: 'default', + label: 'Preview', + reactSource: 'registry/tweenui/slant-morph-button.tsx', + }, + ], + usage: { + react: `import SlantMorphButton from '@/components/tweenui/slant-morph-button'; + +export default function Example() { + return {}}>Discover the library; }`, }, }, diff --git a/apps/docs/registry/tweenui/arrow-tile-button.tsx b/apps/docs/registry/tweenui/arrow-tile-button.tsx new file mode 100644 index 0000000..d297157 --- /dev/null +++ b/apps/docs/registry/tweenui/arrow-tile-button.tsx @@ -0,0 +1,153 @@ +import type { ComponentPropsWithRef, CSSProperties } from 'react'; +import { cn } from '@/lib/utils'; + +export interface ArrowTileButtonProps extends ComponentPropsWithRef<'button'> { + children: string; + variant?: 'tilt' | 'swap'; +} + +function Arrow({ className }: { className?: string }) { + return ( + + ); +} + +interface Wave { + leadMs: number; + spreadMs: number; +} + +function waveDelay(index: number, count: number, { leadMs, spreadMs }: Wave): CSSProperties { + const step = count > 1 ? spreadMs / (count - 1) : 0; + const style: CSSProperties & { '--hop-delay': string } = { + '--hop-delay': `${leadMs + Math.round((count - 1 - index) * step)}ms`, + }; + return style; +} + +const toppleChar = + 'inline-block whitespace-pre transition-none group-[:is(:hover,:focus-visible)]:[translate:0_45%] group-[:is(:hover,:focus-visible)]:[rotate:-14deg] group-[:is(:hover,:focus-visible)]:[scale:1_0.35] group-[:is(:hover,:focus-visible)]:[transform:scale(1,2.8571)_rotate(14deg)_translateY(-45%)] group-[:is(:hover,:focus-visible)]:[transition:translate_145ms_var(--dip)_var(--hop-delay),rotate_145ms_var(--dip)_var(--hop-delay),scale_145ms_var(--dip)_var(--hop-delay),transform_580ms_var(--spring)_calc(var(--hop-delay)+145ms)] motion-reduce:transition-none'; + +const liftChar = + 'inline-block whitespace-pre transition-none group-[:is(:hover,:focus-visible)]:[translate:0_-40%] group-[:is(:hover,:focus-visible)]:[rotate:9deg] group-[:is(:hover,:focus-visible)]:[transform:rotate(-9deg)_translateY(40%)] group-[:is(:hover,:focus-visible)]:[transition:translate_160ms_var(--lift)_var(--hop-delay),rotate_160ms_var(--lift)_var(--hop-delay),transform_560ms_var(--spring)_calc(var(--hop-delay)+160ms)] motion-reduce:transition-none'; + +function WaveLabel({ + children, + charClassName, + wave, +}: { + children: string; + charClassName: string; + wave: Wave; +}) { + const chars = Array.from(children); + return ( + <> + {children} + + + ); +} + +const TOPPLE_WAVE: Wave = { leadMs: 140, spreadMs: 225 }; +const LIFT_WAVE: Wave = { leadMs: 40, spreadMs: 340 }; + +const knockRelease = + 'grid [transition:translate_150ms_var(--soft),rotate_150ms_var(--soft),scale_150ms_var(--soft),transform_150ms_var(--soft)] motion-reduce:transition-none'; + +function TiltContent({ children }: { children: string }) { + return ( + <> + + +