{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion",
  "title": "Motion",
  "description": "MotionProvider (Lenis + GSAP ScrollTrigger), Reveal, SplitHeading, Magnetic, Parallax, Counter, motionTokens and MotionPresets.",
  "dependencies": [
    "@gsap/react",
    "gsap",
    "lenis",
    "motion"
  ],
  "registryDependencies": [
    "@hjconsult/lib"
  ],
  "files": [
    {
      "path": "src/motion/index.ts",
      "content": "'use client'\n\nexport { Counter, type CounterProps } from './counter'\nexport { gsap, gsapEase, registerGsap, ScrollTrigger, SplitText, useGSAP } from './gsap'\nexport { Magnetic, type MagneticProps } from './magnetic'\nexport { Parallax, type ParallaxProps } from './parallax'\nexport {\n  type MotionContextValue,\n  MotionProvider,\n  type MotionProviderProps,\n  type ScrollTarget,\n  useLenis,\n  useMotion,\n} from './provider'\nexport { Reveal, type RevealProps } from './reveal'\nexport { type SplitBy, SplitHeading, type SplitHeadingProps } from './split-heading'\nexport { MotionPresets, motionTokens, REVEAL_DISTANCE } from './tokens'\nexport { useMotionPreference } from './use-motion-preference'\nexport { useReducedMotion } from './use-reduced-motion'\n",
      "type": "registry:lib",
      "target": "hj/motion/index.ts"
    },
    {
      "path": "src/motion/gsap.ts",
      "content": "'use client'\n\nimport { useGSAP } from '@gsap/react'\nimport gsap from 'gsap'\nimport { CustomEase } from 'gsap/CustomEase'\nimport { ScrollTrigger } from 'gsap/ScrollTrigger'\nimport { SplitText } from 'gsap/SplitText'\nimport { type MotionEase, parseCubicBezier, readEase } from '../lib'\n\nlet registered = false\n\n/** Register the GSAP plugins the primitives rely on. Idempotent, browser-only. */\nexport function registerGsap(): void {\n  if (registered || typeof window === 'undefined') return\n  gsap.registerPlugin(useGSAP, ScrollTrigger, SplitText, CustomEase)\n  registered = true\n}\n\nconst easeCache = new Map<MotionEase, string>()\n\n/**\n * A GSAP ease built from the `--hj-motion-ease-*` token. Returns a registered CustomEase id\n * (`hj-out-expo`) or a core ease name when CustomEase is unavailable.\n */\nexport function gsapEase(name: MotionEase): string {\n  const cached = easeCache.get(name)\n  if (cached) return cached\n  registerGsap()\n  const id = `hj-${name}`\n  const points = parseCubicBezier(readEase(name))\n  let ease = 'power3.out'\n  if (points && typeof window !== 'undefined') {\n    try {\n      CustomEase.create(id, points.join(','))\n      ease = id\n    } catch {\n      ease = 'power3.out'\n    }\n  }\n  easeCache.set(name, ease)\n  return ease\n}\n\nexport { gsap, ScrollTrigger, SplitText, useGSAP }\n",
      "type": "registry:lib",
      "target": "hj/motion/gsap.ts"
    },
    {
      "path": "src/motion/tokens.ts",
      "content": "import type { Transition, Variants } from 'motion/react'\nimport {\n  type MotionDuration,\n  type MotionEase,\n  type MotionSpring,\n  parseCubicBezier,\n  readEase,\n  readMotionToken,\n  readSpring,\n} from '../lib'\n\n/**\n * Token accessors shaped for animation libraries. Durations are in **seconds** (GSAP and Motion\n * both use seconds); `readMotionToken` in `@hjconsult/ui/lib` returns milliseconds.\n */\nexport const motionTokens = {\n  /** Seconds. */\n  duration(name: MotionDuration | 'stagger'): number {\n    return readMotionToken(name) / 1000\n  },\n  /** Cubic-bezier control points for the `motion` package (`ease: motionTokens.ease('out-expo')`). */\n  ease(name: MotionEase): [number, number, number, number] {\n    return parseCubicBezier(readEase(name)) ?? [0.16, 1, 0.3, 1]\n  },\n  /** Spring physics for the `motion` package (`transition: { type: 'spring', ...spring('snappy') }`). */\n  spring(name: MotionSpring): { type: 'spring'; stiffness: number; damping: number; mass: number } {\n    return { type: 'spring', ...readSpring(name) }\n  },\n} as const\n\n/** Reveal distance for presets, in px. Matches `Reveal`'s default. */\nexport const REVEAL_DISTANCE = 24\n\nfunction tween(duration: MotionDuration = 'base', ease: MotionEase = 'out-expo'): Transition {\n  return { duration: motionTokens.duration(duration), ease: motionTokens.ease(ease) }\n}\n\n/**\n * Variants for the `motion` package built from tokens **at call time** so they pick up the\n * active theme's values. Use with `initial=\"hidden\" animate=\"visible\"` or `whileInView`.\n */\nexport const MotionPresets = {\n  fadeUp(distance: number = REVEAL_DISTANCE): Variants {\n    return {\n      hidden: { opacity: 0, y: distance },\n      visible: { opacity: 1, y: 0, transition: tween('slow') },\n    }\n  },\n  fadeIn(): Variants {\n    return {\n      hidden: { opacity: 0 },\n      visible: { opacity: 1, transition: tween('base', 'standard') },\n    }\n  },\n  scaleIn(from = 0.96): Variants {\n    return {\n      hidden: { opacity: 0, scale: from },\n      visible: { opacity: 1, scale: 1, transition: tween('slow') },\n    }\n  },\n  staggerContainer(stagger: number = motionTokens.duration('stagger'), delay = 0): Variants {\n    return {\n      hidden: {},\n      visible: { transition: { staggerChildren: stagger, delayChildren: delay } },\n    }\n  },\n} as const\n",
      "type": "registry:lib",
      "target": "hj/motion/tokens.ts"
    },
    {
      "path": "src/motion/provider.tsx",
      "content": "'use client'\n\nimport type { LenisOptions, ScrollToOptions } from 'lenis'\nimport Lenis from 'lenis'\nimport {\n  createContext,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useState,\n  useSyncExternalStore,\n} from 'react'\nimport { prefersReducedMotion } from '../lib'\nimport { gsap, registerGsap, ScrollTrigger } from './gsap'\nimport { useReducedMotion } from './use-reduced-motion'\n\nexport type ScrollTarget = number | string | HTMLElement\n\nexport interface MotionContextValue {\n  /** The Lenis instance, or `null` when smooth scrolling is off (reduced motion, `smooth={false}`). */\n  lenis: Lenis | null\n  /** Live motion preference — the visitor's choice, or the OS. Every effect must respect it. */\n  reduced: boolean\n  /** Scroll to a target; smooth via Lenis when available, native otherwise. */\n  scrollTo: (target: ScrollTarget, options?: ScrollToOptions) => void\n  /** Recompute ScrollTrigger positions (call after layout changes or route transitions). */\n  refresh: () => void\n}\n\n/** What the provider shares: one object for its whole life, whatever Lenis is doing. */\ninterface MotionRuntime {\n  lenis: Lenis | null\n  subscribe: (listener: () => void) => () => void\n  setLenis: (lenis: Lenis | null) => void\n}\n\nfunction createRuntime(): MotionRuntime {\n  const listeners = new Set<() => void>()\n  const runtime: MotionRuntime = {\n    lenis: null,\n    subscribe(listener) {\n      listeners.add(listener)\n      return () => {\n        listeners.delete(listener)\n      }\n    },\n    setLenis(lenis) {\n      runtime.lenis = lenis\n      for (const listener of listeners) listener()\n    },\n  }\n  return runtime\n}\n\nconst MotionContext = createContext<MotionRuntime | null>(null)\n\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect\n\nfunction nativeScrollTo(target: ScrollTarget, options?: ScrollToOptions): void {\n  if (typeof window === 'undefined') return\n  const behavior: ScrollBehavior = options?.immediate || prefersReducedMotion() ? 'auto' : 'smooth'\n  const offset = options?.offset ?? 0\n  if (typeof target === 'number') {\n    window.scrollTo({ top: target + offset, behavior })\n    return\n  }\n  const element = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target\n  if (!element) return\n  const top = element.getBoundingClientRect().top + window.scrollY + offset\n  window.scrollTo({ top, behavior })\n}\n\nexport interface MotionProviderProps {\n  children: ReactNode\n  /** Smooth (Lenis) scrolling. Automatically off under reduced motion. */\n  smooth?: boolean\n  /** Extra Lenis options; `autoRaf` is always `false` because GSAP's ticker drives Lenis. */\n  lenis?: Omit<LenisOptions, 'autoRaf' | 'wrapper' | 'content'>\n}\n\n/**\n * Site-wide motion runtime: registers GSAP plugins, runs Lenis from the GSAP ticker, syncs\n * ScrollTrigger, and exposes `useMotion()`. Mount once in the root layout (client boundary).\n * Sets `html[data-motion=\"on\" | \"reduced\"]` so `[data-reveal]` initial states apply only once JS\n * is running, and so CSS can see the effective preference.\n *\n * Lenis is created in an effect rather than by rendering `<ReactLenis>` conditionally. The\n * conditional render was a different element type in each state, so turning motion on or off\n * unmounted and remounted the entire application below the provider — losing focus, scroll\n * position and every piece of component state. That was invisible while the only way to change\n * the preference was to change an OS setting mid-visit; the motion control makes it one tap.\n *\n * The value it provides never changes identity. It used to carry `lenis` and `reduced`, so it\n * changed twice as a page hydrated: when Lenis started, and when the preference settled. React\n * cannot see inside a Suspense boundary it has not hydrated yet, so it counts a changed value\n * above one as an update to it, and it renders a boundary that is still waiting for its streamed\n * reveal again on the client. The brief composer was drawn a second time beside the server's copy,\n * which stayed hidden in the stream's `S:0` until the reveal's frame came, and under load in WebKit\n * that frame was late enough for a test to find two. `useMotion()` subscribes to Lenis instead, and\n * reads the preference from its own hook.\n */\nexport function MotionProvider({\n  children,\n  smooth = true,\n  lenis: lenisOptions,\n}: MotionProviderProps) {\n  const reduced = useReducedMotion()\n  const enabled = smooth && !reduced\n  const [runtime] = useState(createRuntime)\n\n  // `prefersReducedMotion()` as well, here and for Lenis: the hook still says \"not reduced\" while\n  // hydrating, which marked the page motion-ready and built a Lenis instance for a visitor who\n  // asked for none, only to take both down again a render later.\n  useIsomorphicLayoutEffect(() => {\n    registerGsap()\n    const root = document.documentElement\n    root.dataset.motion = reduced || prefersReducedMotion() ? 'reduced' : 'on'\n    return () => {\n      delete root.dataset.motion\n    }\n  }, [reduced])\n\n  const options = useMemo<LenisOptions>(\n    () => ({ autoRaf: false, anchors: true, ...lenisOptions }),\n    [lenisOptions],\n  )\n\n  useEffect(() => {\n    if (!enabled || prefersReducedMotion()) return\n    registerGsap()\n    const instance = new Lenis(options)\n    const update = (time: number) => instance.raf(time * 1000)\n    instance.on('scroll', ScrollTrigger.update)\n    gsap.ticker.add(update)\n    gsap.ticker.lagSmoothing(0)\n    runtime.setLenis(instance)\n    return () => {\n      instance.off('scroll', ScrollTrigger.update)\n      gsap.ticker.remove(update)\n      instance.destroy()\n      runtime.setLenis(null)\n      // Lenis leaves the page mid-scroll otherwise: hand the browser back a normal document.\n      ScrollTrigger.refresh()\n    }\n  }, [enabled, options, runtime])\n\n  return <MotionContext.Provider value={runtime}>{children}</MotionContext.Provider>\n}\n\nfunction refresh(): void {\n  if (typeof window !== 'undefined') ScrollTrigger.refresh()\n}\n\nconst noSubscription = () => () => {}\n\n/** Motion runtime. Works without a provider (native scrolling, no Lenis). */\nexport function useMotion(): MotionContextValue {\n  const runtime = useContext(MotionContext)\n  const lenis = useSyncExternalStore(\n    runtime?.subscribe ?? noSubscription,\n    () => runtime?.lenis ?? null,\n    () => null,\n  )\n  const reduced = useReducedMotion()\n  return useMemo<MotionContextValue>(\n    () => ({\n      lenis,\n      reduced,\n      scrollTo: (target, options) => {\n        if (lenis) lenis.scrollTo(target, { ...options, immediate: options?.immediate ?? reduced })\n        else nativeScrollTo(target, options)\n      },\n      refresh,\n    }),\n    [lenis, reduced],\n  )\n}\n\n/** The active Lenis instance, or `undefined` when smooth scrolling is off. */\nexport function useLenis(): Lenis | undefined {\n  return useMotion().lenis ?? undefined\n}\n",
      "type": "registry:lib",
      "target": "hj/motion/provider.tsx"
    },
    {
      "path": "src/motion/use-reduced-motion.ts",
      "content": "'use client'\n\nimport { useSyncExternalStore } from 'react'\nimport {\n  prefersReducedMotion,\n  REDUCED_MOTION_QUERY,\n  subscribeMotionPreference,\n} from '../lib/motion-preference'\n\n/**\n * Two things can change the answer: the operating system's setting, and the visitor's own choice\n * on this site. Watch both — a stored choice that only took effect on reload would feel broken.\n */\nfunction subscribe(onChange: () => void): () => void {\n  const unsubscribePreference = subscribeMotionPreference(onChange)\n  if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {\n    return unsubscribePreference\n  }\n  const query = window.matchMedia(REDUCED_MOTION_QUERY)\n  query.addEventListener('change', onChange)\n  return () => {\n    query.removeEventListener('change', onChange)\n    unsubscribePreference()\n  }\n}\n\nfunction getServerSnapshot(): boolean {\n  return false\n}\n\n/**\n * Whether motion should be held back: the visitor's explicit choice when they made one, the OS\n * `prefers-reduced-motion` otherwise. Server renders assume motion is allowed; the value settles\n * right after hydration and updates whenever either input changes.\n */\nexport function useReducedMotion(): boolean {\n  return useSyncExternalStore(subscribe, prefersReducedMotion, getServerSnapshot)\n}\n",
      "type": "registry:lib",
      "target": "hj/motion/use-reduced-motion.ts"
    },
    {
      "path": "src/motion/reveal.tsx",
      "content": "'use client'\n\nimport type { ComponentPropsWithoutRef, ElementType, Ref } from 'react'\nimport { useRef } from 'react'\nimport { cn, type MotionDuration, prefersReducedMotion, readMotionToken } from '../lib'\nimport { gsap, gsapEase, registerGsap, useGSAP } from './gsap'\nimport { REVEAL_DISTANCE } from './tokens'\nimport { useReducedMotion } from './use-reduced-motion'\n\nexport interface RevealProps extends ComponentPropsWithoutRef<'div'> {\n  as?: ElementType\n  /** Delay in ms. */\n  delay?: number\n  /** Vertical offset in px. Defaults to `distance`. */\n  y?: number\n  /** Travel distance in px (both axes default to this). */\n  distance?: number\n  /** Play once and stay visible (default) or reverse when scrolled back out. */\n  once?: boolean\n  /** Stagger in ms between children marked `data-reveal-item`. Falls back to the token. */\n  stagger?: number | boolean\n  /** Duration token or ms. */\n  duration?: MotionDuration | number\n  /** Soft-focus in from a blur. */\n  blur?: boolean\n  /** Wipe in from the bottom with a clip-path. */\n  clip?: boolean\n  /** ScrollTrigger start, e.g. `'top 85%'`. */\n  start?: string\n  ref?: Ref<HTMLDivElement>\n}\n\n/** Fraction of the viewport height at which the reveal starts. */\n/** Elements already inside the viewport on load must reveal without a scroll. */\nconst DEFAULT_START = 'top 95%'\n\n/**\n * Scroll-triggered entrance. Content is fully visible in SSR HTML and under reduced motion;\n * GSAP only ever animates *from* a transparent state once JS runs.\n *\n * Transparent, never hidden. `visibility: hidden` (GSAP's `autoAlpha`) takes content out of the\n * tab order and the accessibility tree until something scrolls it into view, and a keyboard or\n * screen reader user is exactly who does not: from a fresh load, 438 of the site's 1,452 tab\n * stops could not be reached that way. Waiting content keeps its place in both, and the moment\n * focus lands inside it the entrance finishes, so what has focus can be seen.\n */\nexport function Reveal({\n  as = 'div',\n  delay = 0,\n  y,\n  distance = REVEAL_DISTANCE,\n  once = true,\n  stagger,\n  duration = 'slow',\n  blur = false,\n  clip = false,\n  start = DEFAULT_START,\n  className,\n  children,\n  ref,\n  ...props\n}: RevealProps) {\n  // R3F augments JSX.IntrinsicElements in consumers; a concrete tag keeps props typed.\n  const Tag = as as 'div'\n  const scope = useRef<HTMLElement>(null)\n  const reduced = useReducedMotion()\n\n  useGSAP(\n    () => {\n      const el = scope.current\n      if (!el) return\n      // The hook answers \"not reduced\" while hydrating, before it has read the preference, and\n      // this effect runs in that pass. Ask the document as well, or the entrance is set up for a\n      // visitor who asked for none and nothing takes it down again.\n      if (reduced || prefersReducedMotion()) {\n        el.dataset.reveal = 'done'\n        return\n      }\n      registerGsap()\n      const items = el.querySelectorAll<HTMLElement>('[data-reveal-item]')\n      const targets: Element | NodeListOf<HTMLElement> = stagger && items.length ? items : el\n      const staggerSeconds =\n        stagger === false || stagger === undefined\n          ? 0\n          : (typeof stagger === 'number' ? stagger : readMotionToken('stagger')) / 1000\n      const seconds = (typeof duration === 'number' ? duration : readMotionToken(duration)) / 1000\n      const offset = y ?? distance\n\n      // Mark as ready so the CSS pre-hydration state can hand over to inline styles, and as\n      // waiting, with the line it waits for, until the entrance starts. `e2e/settle.ts` reads\n      // both: waiting is not mid-fade, and still waiting once its top has passed that line is\n      // stuck.\n      el.dataset.reveal = 'done'\n      el.dataset.revealPending = start\n      gsap.set(targets, {\n        opacity: 0,\n        y: offset,\n        ...(blur ? { filter: 'blur(12px)' } : {}),\n        ...(clip ? { clipPath: 'inset(0 0 100% 0)' } : {}),\n      })\n      const entrance = gsap.to(targets, {\n        opacity: 1,\n        y: 0,\n        ...(blur ? { filter: 'blur(0px)' } : {}),\n        ...(clip ? { clipPath: 'inset(0 0 0% 0)' } : {}),\n        duration: seconds,\n        delay: delay / 1000,\n        stagger: staggerSeconds,\n        ease: gsapEase('out-expo'),\n        // Only pass clearProps when there is something to clear: GSAP's plugin\n        // splits the value string and throws on `undefined`.\n        ...(blur || clip ? { clearProps: 'filter,clipPath' } : {}),\n        onStart: () => {\n          delete el.dataset.revealPending\n        },\n        scrollTrigger: {\n          trigger: el,\n          start,\n          once,\n          toggleActions: once ? 'play none none none' : 'play none none reverse',\n        },\n      })\n\n      // Tab and a screen reader's cursor reach content before any scroll does. Finish the\n      // entrance as focus arrives, so it never lands on something still transparent, and hold\n      // it there: paused, because a reveal that reverses may be on its way out. The trigger goes\n      // last and keeps the tween (`allowAnimation`): killing a ScrollTrigger kills its tween by\n      // default, a killed tween renders nothing, and what has focus would stay transparent.\n      const show = () => {\n        entrance.progress(1).pause()\n        entrance.scrollTrigger?.kill(false, true)\n        delete el.dataset.revealPending\n      }\n      el.addEventListener('focusin', show)\n      return () => {\n        el.removeEventListener('focusin', show)\n        delete el.dataset.revealPending\n      }\n    },\n    {\n      scope,\n      dependencies: [reduced, once, stagger, delay, duration, blur, clip, start, y, distance],\n      // Undo the entrance when the preference changes, whichever way: the footer switch changes\n      // it mid-visit, and an entrance left behind keeps moving for someone who turned motion off.\n      revertOnUpdate: true,\n    },\n  )\n\n  return (\n    <Tag\n      ref={(node: HTMLElement | null) => {\n        scope.current = node\n        if (typeof ref === 'function') ref(node as HTMLDivElement)\n        else if (ref) ref.current = node as HTMLDivElement\n      }}\n      data-reveal=\"\"\n      className={cn('will-change-[opacity,transform]', className)}\n      {...props}\n    >\n      {children}\n    </Tag>\n  )\n}\n",
      "type": "registry:lib",
      "target": "hj/motion/reveal.tsx"
    },
    {
      "path": "src/motion/split-heading.tsx",
      "content": "'use client'\n\nimport type { ComponentPropsWithoutRef, Ref } from 'react'\nimport { useRef } from 'react'\nimport { cn, type MotionDuration, prefersReducedMotion, readMotionToken } from '../lib'\nimport { gsap, gsapEase, registerGsap, SplitText, useGSAP } from './gsap'\nimport { useReducedMotion } from './use-reduced-motion'\n\nexport type SplitBy = 'chars' | 'words' | 'lines'\n\nexport interface SplitHeadingProps extends Omit<ComponentPropsWithoutRef<'h1'>, 'children'> {\n  as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span'\n  /** Plain text only — it stays in the DOM for SSR, SEO and screen readers. */\n  children: string\n  by?: SplitBy\n  /** Clip each unit so it rises out of a mask. */\n  mask?: boolean\n  /** Stagger in ms between units. Falls back to the token. */\n  stagger?: number\n  /** Delay in ms. */\n  delay?: number\n  /** Animate on mount, or when scrolled into view. */\n  trigger?: 'mount' | 'scroll'\n  duration?: MotionDuration | number\n  ref?: Ref<HTMLHeadingElement>\n}\n\n/** Percent of the unit's own height it travels from. */\nconst TRAVEL_PERCENT = 110\n\n/** What ARIA lets an author name. A paragraph or a span may not carry a label. */\nconst NAMEABLE = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])\n\n/**\n * Masked text reveal using GSAP SplitText. The original string is rendered as plain text, so the\n * heading is complete without JS, and SplitText reverts on unmount.\n *\n * On a heading, SplitText names it with its own text and hides the generated spans from\n * assistive technology. On a `p` or `span` that name is prohibited, and a screen reader may skip\n * it and find every word under it hidden, so there the spans stay readable instead. Words and\n * lines read as they are; split those by words or lines, not characters.\n */\nexport function SplitHeading({\n  as: Tag = 'h2',\n  children,\n  by = 'lines',\n  mask = true,\n  stagger,\n  delay = 0,\n  trigger = 'scroll',\n  duration = 'slow',\n  className,\n  ref,\n  ...props\n}: SplitHeadingProps) {\n  const scope = useRef<HTMLHeadingElement>(null)\n  const reduced = useReducedMotion()\n\n  useGSAP(\n    () => {\n      const el = scope.current\n      // `prefersReducedMotion()` as well: the hook still says \"not reduced\" while hydrating.\n      if (!el || reduced || prefersReducedMotion()) return\n      registerGsap()\n      const seconds = (typeof duration === 'number' ? duration : readMotionToken(duration)) / 1000\n      const staggerSeconds = (stagger ?? readMotionToken('stagger')) / 1000\n      const unit = by === 'chars' ? 'char' : by === 'words' ? 'word' : 'line'\n      const split = SplitText.create(el, {\n        type: by === 'lines' ? 'lines' : `${by},lines`,\n        mask: mask ? by : undefined,\n        autoSplit: by === 'lines',\n        aria: NAMEABLE.has(Tag) ? 'auto' : 'none',\n        linesClass: 'hj-split-line',\n        wordsClass: 'hj-split-word',\n        charsClass: 'hj-split-char',\n        onSplit(self) {\n          const targets = self[by]\n          return gsap.from(targets, {\n            yPercent: TRAVEL_PERCENT,\n            opacity: mask ? 1 : 0,\n            duration: seconds,\n            delay: delay / 1000,\n            stagger: staggerSeconds,\n            ease: gsapEase('out-expo'),\n            ...(trigger === 'scroll'\n              ? { scrollTrigger: { trigger: el, start: 'top 85%', once: true } }\n              : {}),\n            onComplete: () => {\n              el.dataset.split = 'done'\n            },\n          })\n        },\n      })\n      el.dataset.split = unit\n      return () => {\n        split.revert()\n        delete el.dataset.split\n      }\n    },\n    { scope, dependencies: [reduced, by, mask, stagger, delay, trigger, duration, children] },\n  )\n\n  return (\n    <Tag\n      ref={(node: HTMLHeadingElement | null) => {\n        scope.current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) ref.current = node\n      }}\n      className={cn('[&_.hj-split-line]:will-change-transform', className)}\n      {...props}\n    >\n      {children}\n    </Tag>\n  )\n}\n",
      "type": "registry:lib",
      "target": "hj/motion/split-heading.tsx"
    },
    {
      "path": "src/motion/magnetic.tsx",
      "content": "'use client'\n\nimport type { ComponentPropsWithoutRef, Ref } from 'react'\nimport { useRef } from 'react'\nimport { cn, hasCoarsePointer, prefersReducedMotion, readMotionToken } from '../lib'\nimport { gsap, gsapEase, registerGsap, useGSAP } from './gsap'\nimport { useReducedMotion } from './use-reduced-motion'\n\nexport interface MagneticProps extends ComponentPropsWithoutRef<'span'> {\n  /** How far the element follows the pointer, as a fraction of the pointer offset. */\n  strength?: number\n  /** Activation radius in px measured from the element's centre. */\n  radius?: number\n  ref?: Ref<HTMLSpanElement>\n}\n\n/** Default pull as a fraction of pointer distance. */\nconst DEFAULT_STRENGTH = 0.35\n/** Default activation radius in px. */\nconst DEFAULT_RADIUS = 120\n\n/**\n * Pointer-follow wrapper: the child leans toward a nearby cursor and springs back elastically.\n * Inert on touch devices and under reduced motion.\n */\nexport function Magnetic({\n  strength = DEFAULT_STRENGTH,\n  radius = DEFAULT_RADIUS,\n  className,\n  children,\n  ref,\n  ...props\n}: MagneticProps) {\n  const scope = useRef<HTMLSpanElement>(null)\n  const reduced = useReducedMotion()\n\n  useGSAP(\n    () => {\n      const el = scope.current\n      // `prefersReducedMotion()` as well: the hook still says \"not reduced\" while hydrating.\n      if (!el || reduced || prefersReducedMotion() || hasCoarsePointer()) return\n      registerGsap()\n      const follow = { duration: readMotionToken('slow') / 1000, ease: gsapEase('out-expo') }\n      const xTo = gsap.quickTo(el, 'x', follow)\n      const yTo = gsap.quickTo(el, 'y', follow)\n      let active = false\n      let settle: gsap.core.Tween | null = null\n\n      const release = () => {\n        if (!active) return\n        active = false\n        settle = gsap.to(el, {\n          x: 0,\n          y: 0,\n          duration: readMotionToken('hero') / 1000,\n          ease: 'elastic.out(1, 0.45)',\n          overwrite: 'auto',\n        })\n      }\n\n      const onMove = (event: PointerEvent) => {\n        const rect = el.getBoundingClientRect()\n        const dx = event.clientX - (rect.left + rect.width / 2)\n        const dy = event.clientY - (rect.top + rect.height / 2)\n        const distance = Math.hypot(dx, dy)\n        if (distance < radius) {\n          settle?.kill()\n          settle = null\n          active = true\n          xTo(dx * strength)\n          yTo(dy * strength)\n        } else {\n          release()\n        }\n      }\n\n      window.addEventListener('pointermove', onMove, { passive: true })\n      window.addEventListener('pointerleave', release)\n      return () => {\n        window.removeEventListener('pointermove', onMove)\n        window.removeEventListener('pointerleave', release)\n      }\n    },\n    { scope, dependencies: [reduced, strength, radius], revertOnUpdate: true },\n  )\n\n  return (\n    <span\n      ref={(node) => {\n        scope.current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) ref.current = node\n      }}\n      data-magnetic=\"\"\n      className={cn('inline-flex will-change-transform', className)}\n      {...props}\n    >\n      {children}\n    </span>\n  )\n}\n",
      "type": "registry:lib",
      "target": "hj/motion/magnetic.tsx"
    },
    {
      "path": "src/motion/parallax.tsx",
      "content": "'use client'\n\nimport type { ComponentPropsWithoutRef, ElementType, Ref } from 'react'\nimport { useRef } from 'react'\nimport { cn, prefersReducedMotion } from '../lib'\nimport { gsap, registerGsap, useGSAP } from './gsap'\nimport { useReducedMotion } from './use-reduced-motion'\n\nexport interface ParallaxProps extends ComponentPropsWithoutRef<'div'> {\n  as?: ElementType\n  /**\n   * Travel as a fraction of the element's own size while it crosses the viewport.\n   * Positive values lag behind the scroll, negative values lead it.\n   */\n  speed?: number\n  axis?: 'x' | 'y'\n  ref?: Ref<HTMLDivElement>\n}\n\n/** Default travel: 20% of the element's own size. */\nconst DEFAULT_SPEED = 0.2\n\n/** Scroll-scrubbed offset. Static under reduced motion. */\nexport function Parallax({\n  as = 'div',\n  speed = DEFAULT_SPEED,\n  axis = 'y',\n  className,\n  children,\n  ref,\n  ...props\n}: ParallaxProps) {\n  // R3F augments JSX.IntrinsicElements in consumers; a concrete tag keeps props typed.\n  const Tag = as as 'div'\n  const scope = useRef<HTMLElement>(null)\n  const reduced = useReducedMotion()\n\n  useGSAP(\n    () => {\n      const el = scope.current\n      // `prefersReducedMotion()` as well: the hook still says \"not reduced\" while hydrating.\n      if (!el || reduced || prefersReducedMotion()) return\n      registerGsap()\n      const property = axis === 'x' ? 'xPercent' : 'yPercent'\n      const amount = speed * 100\n      gsap.fromTo(\n        el,\n        { [property]: -amount },\n        {\n          [property]: amount,\n          ease: 'none',\n          scrollTrigger: { trigger: el, start: 'top bottom', end: 'bottom top', scrub: true },\n        },\n      )\n    },\n    { scope, dependencies: [reduced, speed, axis], revertOnUpdate: true },\n  )\n\n  return (\n    <Tag\n      ref={(node: HTMLElement | null) => {\n        scope.current = node\n        if (typeof ref === 'function') ref(node as HTMLDivElement)\n        else if (ref) ref.current = node as HTMLDivElement\n      }}\n      data-parallax=\"\"\n      className={cn('will-change-transform', className)}\n      {...props}\n    >\n      {children}\n    </Tag>\n  )\n}\n",
      "type": "registry:lib",
      "target": "hj/motion/parallax.tsx"
    },
    {
      "path": "src/motion/counter.tsx",
      "content": "'use client'\n\nimport type { ComponentPropsWithoutRef, Ref } from 'react'\nimport { useMemo, useRef } from 'react'\nimport { cn, type MotionDuration, prefersReducedMotion, readMotionToken } from '../lib'\nimport { gsap, gsapEase, registerGsap, useGSAP } from './gsap'\nimport { useReducedMotion } from './use-reduced-motion'\n\nexport interface CounterProps extends Omit<ComponentPropsWithoutRef<'span'>, 'children'> {\n  to: number\n  from?: number\n  /** Duration token or ms. */\n  duration?: MotionDuration | number\n  /** Delay in ms. */\n  delay?: number\n  /** Formatter, or `Intl.NumberFormat` options (uses `locale`). */\n  format?: ((value: number) => string) | Intl.NumberFormatOptions\n  locale?: string\n  /** Start when scrolled into view (default) or on mount. */\n  trigger?: 'scroll' | 'mount'\n  ref?: Ref<HTMLSpanElement>\n}\n\nfunction decimalsOf(value: number): number {\n  if (!Number.isFinite(value)) return 0\n  const text = String(value)\n  const index = text.indexOf('.')\n  return index === -1 ? 0 : text.length - index - 1\n}\n\n/**\n * Animated number. The final value is rendered on the server so the figure is always present;\n * the count-up runs once on enter (skipped under reduced motion).\n */\nexport function Counter({\n  to,\n  from = 0,\n  duration = 'hero',\n  delay = 0,\n  format,\n  locale,\n  trigger = 'scroll',\n  className,\n  ref,\n  ...props\n}: CounterProps) {\n  const scope = useRef<HTMLSpanElement>(null)\n  const reduced = useReducedMotion()\n  const formatter = useMemo(() => {\n    if (typeof format === 'function') return format\n    const numberFormat = new Intl.NumberFormat(locale, {\n      maximumFractionDigits: decimalsOf(to),\n      ...format,\n    })\n    return (value: number) => numberFormat.format(value)\n  }, [format, locale, to])\n\n  useGSAP(\n    () => {\n      const el = scope.current\n      // `prefersReducedMotion()` as well: the hook still says \"not reduced\" while hydrating.\n      if (!el || reduced || prefersReducedMotion()) return\n      registerGsap()\n      const seconds = (typeof duration === 'number' ? duration : readMotionToken(duration)) / 1000\n      const state = { value: from }\n      const final = formatter(to)\n      el.setAttribute('aria-hidden', 'true')\n      el.textContent = formatter(from)\n      gsap.to(state, {\n        value: to,\n        duration: seconds,\n        delay: delay / 1000,\n        ease: gsapEase('out-expo'),\n        onUpdate: () => {\n          el.textContent = formatter(state.value)\n        },\n        onComplete: () => {\n          el.textContent = final\n          el.removeAttribute('aria-hidden')\n        },\n        ...(trigger === 'scroll'\n          ? { scrollTrigger: { trigger: el, start: 'top 90%', once: true } }\n          : {}),\n      })\n    },\n    { scope, dependencies: [reduced, to, from, duration, delay, trigger, formatter] },\n  )\n\n  return (\n    <span\n      ref={(node) => {\n        scope.current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) ref.current = node\n      }}\n      data-counter=\"\"\n      className={cn('tabular-nums', className)}\n      {...props}\n    >\n      {formatter(to)}\n    </span>\n  )\n}\n",
      "type": "registry:lib",
      "target": "hj/motion/counter.tsx"
    }
  ],
  "cssVars": {},
  "type": "registry:lib"
}