{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "all",
  "title": "blaze-motion",
  "description": "The full motion engine in one install — the tuned tokens (lib/motion.ts), the provider, and every motion primitive. Self-contained; retune the whole set from the `feel` block in lib/motion.ts.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "lib/motion.ts",
      "content": "import type { TargetAndTransition, Transition, Variants } from \"motion/react\";\n\n/* ─────────────────────────────────────────────────────────────────────\n *  TUNE HERE — the whole engine's feel, in one place.\n *\n *  This is the only block you edit to retune blaze-motion for a project.\n *  Slower project? raise the durations. Faster? lower them. Every\n *  primitive derives from these values, so one edit re-tunes them all.\n *\n *  Per-instance overrides still win: <Fade delay={0.1} /> etc.\n * ───────────────────────────────────────────────────────────────────── */\nexport const feel = {\n  /** seconds — the three speeds the engine animates at */\n  duration: { fast: 0.2, base: 0.55, slow: 0.8 },\n  /** the tuned ease-out curve — a straight settle, no spring overshoot */\n  ease: [0.22, 0.61, 0.36, 1],\n  /** the softer ease used for page transitions */\n  easeSoft: [0.4, 0, 0.2, 1],\n  /** px a <StaggerItem> rises from (also Slide's `base` distance tier) */\n  rise: 28,\n  /** px a <Fade> drifts from — a FIXED, subtle reveal drift (Slide owns tunable travel) */\n  fadeShift: 16,\n  /** px a <Fade blur> focus-settles from — blur(Npx) → 0, opt-in (`filter` is GPU-costly) */\n  fadeBlur: 8,\n  /** how much of an element must be in view before it animates (0–1) */\n  inView: 0.3,\n  /** seconds between staggered children */\n  stagger: 0.08,\n  /** seconds before the first staggered child */\n  staggerDelay: 0.05,\n  /** seconds between staggered words (TextReveal, per-word) */\n  textStagger: 0.04,\n  /** seconds between staggered chars (TextReveal, per-char) */\n  textStaggerChar: 0.02,\n  /** px a TextReveal word/char rises from — shorter than a section rise */\n  textRise: 12,\n  /** spring pop — the ONE deliberate overshoot (SpringPop/RadialStagger); never titles */\n  spring: { stiffness: 300, damping: 15 },\n  /** scale a ScaleIn tween settles up FROM (tween, never spring — that's SpringPop) */\n  scaleFrom: 0.95,\n  /** Slide translate TIERS in px — `base` reuses `rise` (28); see `slideDistance` */\n  distance: { tight: 8, hero: 64 },\n  /** seconds between staggered lines (LineReveal / MaskTextReveal by line) */\n  lineStagger: 0.08,\n  /** px a LineReveal line rises from — between a word rise and a section rise */\n  lineRise: 14,\n  /** scale a GrowFromOrigin element grows up FROM, anchored to its transform-origin */\n  growScale: 0.85,\n  /** degrees a PerspectiveTiltIn element rotates on X from → 0 */\n  tiltAngle: 8,\n  /** px transform-perspective depth for PerspectiveTiltIn's rotateX */\n  perspective: 800,\n  /** percent a InsetFrameReveal clip-path insets uniformly from → 0% */\n  frameInset: 15,\n} as const;\n/* ───────────────────────────────────────────────────────────────────── */\n\n// Derived tokens — you rarely touch below this line.\n\nexport const durations = feel.duration;\n\nexport const ease = { out: feel.ease, soft: feel.easeSoft } as const;\n\nexport const revealTransition = {\n  duration: feel.duration.base,\n  ease: feel.ease,\n} as const satisfies Transition;\n\nexport const viewportOnce = { once: true, amount: feel.inView } as const;\n\nexport const fade = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n} as const;\n\n/** 5 directions a <Fade> drifts from as it fades in; `\"none\"` = pure opacity, no transform. */\nexport type FadeDirection = \"none\" | \"up\" | \"down\" | \"left\" | \"right\";\n\n// Unit sign per direction → the offset it drifts FROM (× feel.fadeShift). Mirrors slideOffset.\nconst fadeOffset: Record<FadeDirection, { x: number; y: number }> = {\n  none: { x: 0, y: 0 },\n  up: { x: 0, y: 1 },\n  down: { x: 0, y: -1 },\n  left: { x: 1, y: 0 },\n  right: { x: -1, y: 0 },\n};\n\n/**\n * <Fade> variants — fade + a FIXED subtle drift from `direction`, plus an optional\n * `blur`px → 0 focus-settle layered on top. The drift is fixed at `feel.fadeShift`\n * (tunable travel is Slide's job). No transition is baked in — the component folds\n * `delay` into `animate.transition` itself (the L004 variant-transition idiom).\n */\nexport const fadeVariants = (direction: FadeDirection = \"up\", blur = 0): Variants => {\n  const { x, y } = fadeOffset[direction];\n  const initial: TargetAndTransition = { opacity: 0, x: x * feel.fadeShift, y: y * feel.fadeShift };\n  const animate: TargetAndTransition = { opacity: 1, x: 0, y: 0 };\n  if (blur > 0) {\n    initial.filter = `blur(${blur}px)`;\n    animate.filter = \"blur(0px)\";\n  }\n  return { initial, animate };\n};\n\n/** scale-DOWN settle — for LARGE images only (never titles/text). */\nexport const cinematicScale = {\n  initial: { opacity: 0, scale: 1.1 },\n  animate: { opacity: 1, scale: 1 },\n} as const;\n\nexport const staggerContainer: Variants = {\n  initial: {},\n  animate: {\n    transition: { staggerChildren: feel.stagger, delayChildren: feel.staggerDelay },\n  },\n};\n\nexport const staggerItem: Variants = {\n  initial: { opacity: 0, y: feel.rise },\n  animate: { opacity: 1, y: 0, transition: revealTransition },\n};\n\n/** spring overshoot — the 0.8 → ~1.05 → 1 pop comes from the spring itself. */\nexport const springPopTransition = {\n  type: \"spring\",\n  stiffness: feel.spring.stiffness,\n  damping: feel.spring.damping,\n} as const satisfies Transition;\n\nexport const springPop = {\n  initial: { opacity: 0, scale: 0.8 },\n  animate: { opacity: 1, scale: 1 },\n} as const;\n\n/** per-word / per-char reveal — STRAIGHT rise, NO scale (the title rule). */\nexport const textRevealItem: Variants = {\n  initial: { opacity: 0, y: feel.textRise },\n  animate: { opacity: 1, y: 0, transition: revealTransition },\n};\n\n// Factory: TextReveal tunes its step per word vs char, plus an optional lead delay.\nexport const textRevealContainer = (\n  staggerChildren: number = feel.textStagger,\n  delayChildren = 0,\n): Variants => ({\n  initial: {},\n  animate: { transition: { staggerChildren, delayChildren } },\n});\n\n/* ── Entrance components (CARD-025 wave) — all straight `feel.ease` tweens ── */\n\n/** 8 travel directions a <Slide> enters along (diagonals move on both axes). */\nexport type SlideDirection =\n  | \"up\"\n  | \"down\"\n  | \"left\"\n  | \"right\"\n  | \"up-left\"\n  | \"up-right\"\n  | \"down-left\"\n  | \"down-right\";\n\n/** The three <Slide> distance tiers — `base` reuses `feel.rise`. */\nexport const slideDistance = {\n  tight: feel.distance.tight,\n  base: feel.rise,\n  hero: feel.distance.hero,\n} as const;\n\n// Unit sign per direction — travel dir → the offset it enters FROM (× distance).\nconst slideOffset: Record<SlideDirection, { x: number; y: number }> = {\n  up: { x: 0, y: 1 },\n  down: { x: 0, y: -1 },\n  left: { x: 1, y: 0 },\n  right: { x: -1, y: 0 },\n  \"up-left\": { x: 1, y: 1 },\n  \"up-right\": { x: -1, y: 1 },\n  \"down-left\": { x: 1, y: -1 },\n  \"down-right\": { x: -1, y: -1 },\n};\n\n/** fade + translate from `direction`; `distance` px defaults to the `base` tier. */\nexport const slideVariants = (\n  direction: SlideDirection,\n  distance: number = feel.rise,\n): Variants => {\n  const { x, y } = slideOffset[direction];\n  return {\n    initial: { opacity: 0, x: x * distance, y: y * distance },\n    animate: { opacity: 1, x: 0, y: 0, transition: revealTransition },\n  };\n};\n\n/** fade + scale-UP settle — a plain ease-out TWEEN (never a spring; that's SpringPop). */\nexport const scaleIn = {\n  initial: { opacity: 0, scale: feel.scaleFrom },\n  animate: { opacity: 1, scale: 1 },\n} as const;\n\n/** 4 edges a <ClipReveal> wipe reveals along; the box stays put. */\nexport type WipeDirection = \"up\" | \"down\" | \"left\" | \"right\";\n\n// inset(T R B L) — one edge starts fully clipped (100%) and retracts to 0%.\n// All four strings share identical numeric slots so Motion diffs them cleanly.\nconst wipeInset: Record<WipeDirection, string> = {\n  up: \"inset(100% 0% 0% 0%)\",\n  down: \"inset(0% 0% 100% 0%)\",\n  left: \"inset(0% 0% 0% 100%)\",\n  right: \"inset(0% 100% 0% 0%)\",\n};\n\n/** clip-path edge wipe from `direction` → fully open `inset(0% 0% 0% 0%)`. */\nexport const wipeVariants = (direction: WipeDirection): Variants => ({\n  initial: { clipPath: wipeInset[direction] },\n  animate: { clipPath: \"inset(0% 0% 0% 0%)\", transition: revealTransition },\n});\n\n/** per-line rise + fade — one <LineReveal> line. Pair with `lineRevealContainer`. */\nexport const lineRevealItem: Variants = {\n  initial: { opacity: 0, y: feel.lineRise },\n  animate: { opacity: 1, y: 0, transition: revealTransition },\n};\n\n/** <LineReveal> container — reuses the text-stagger factory at the line step. */\nexport const lineRevealContainer = (delayChildren = 0): Variants =>\n  textRevealContainer(feel.lineStagger, delayChildren);\n\n/** transform-origin anchors a <GrowFromOrigin> grows from. */\nexport type Origin =\n  | \"top\"\n  | \"top-left\"\n  | \"top-right\"\n  | \"bottom\"\n  | \"bottom-left\"\n  | \"bottom-right\"\n  | \"left\"\n  | \"right\"\n  | \"center\";\n\n/** Origin token → CSS `transform-origin` string. */\nexport const originMap: Record<Origin, string> = {\n  top: \"top\",\n  \"top-left\": \"top left\",\n  \"top-right\": \"top right\",\n  bottom: \"bottom\",\n  \"bottom-left\": \"bottom left\",\n  \"bottom-right\": \"bottom right\",\n  left: \"left\",\n  right: \"right\",\n  center: \"center\",\n};\n\n/** fade + scale-UP from `origin`; transform-origin rides `style` (static, unanimated). */\nexport const growVariants = (origin: Origin = \"center\"): Variants => ({\n  initial: { opacity: 0, scale: feel.growScale, transformOrigin: originMap[origin] },\n  animate: {\n    opacity: 1,\n    scale: 1,\n    transformOrigin: originMap[origin],\n    transition: revealTransition,\n  },\n});\n\n/** fade + scaleX 0 → 1 from the left — a rule / underline draw. */\nexport const scaleXReveal = {\n  initial: { opacity: 0, scaleX: 0, transformOrigin: \"left\" },\n  animate: { opacity: 1, scaleX: 1, transformOrigin: \"left\" },\n} as const;\n\n/** fade + rise + rotateX settle over a perspective — a subtle 3D tilt-in. */\nexport const perspectiveTilt = {\n  initial: {\n    opacity: 0,\n    y: feel.rise,\n    rotateX: feel.tiltAngle,\n    transformPerspective: feel.perspective,\n  },\n  animate: {\n    opacity: 1,\n    y: 0,\n    rotateX: 0,\n    transformPerspective: feel.perspective,\n  },\n} as const;\n\n/** clip-path inset uniform `feel.frameInset`% → 0% — a frame that opens outward. */\nexport const insetFrameReveal = {\n  initial: {\n    clipPath: `inset(${feel.frameInset}% ${feel.frameInset}% ${feel.frameInset}% ${feel.frameInset}%)`,\n  },\n  animate: { clipPath: \"inset(0% 0% 0% 0%)\" },\n} as const;\n\n/** masked line/word — inner rise from below its overflow-hidden wrapper, NO opacity. */\nexport const maskTextItem: Variants = {\n  initial: { y: \"110%\" },\n  animate: { y: 0, transition: revealTransition },\n};\n\n/** fade + filter grayscale(1) → grayscale(0) — slow, images only (`filter` is costly). */\nexport const grayscaleReveal = {\n  initial: { opacity: 0, filter: \"grayscale(1)\" },\n  animate: { opacity: 1, filter: \"grayscale(0)\" },\n} as const;\n",
      "type": "registry:lib",
      "target": "@lib/motion.ts"
    },
    {
      "path": "components/motion/motion-provider.tsx",
      "content": "\"use client\";\n\nimport { domAnimation, LazyMotion, MotionConfig } from \"motion/react\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Mount ONCE in your root layout, wrapping {children}.\n * LazyMotion `strict` means primitives must import `m` from \"motion/react-m\" —\n * a raw `motion.*` will throw. `reducedMotion=\"user\"` honors the OS setting.\n */\nexport function MotionProvider({ children }: { children: ReactNode }) {\n  return (\n    <LazyMotion features={domAnimation} strict>\n      <MotionConfig reducedMotion=\"user\">{children}</MotionConfig>\n    </LazyMotion>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/motion-provider.tsx"
    },
    {
      "path": "components/motion/cinematic-image.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cinematicScale, durations, ease, viewportOnce } from \"@/lib/motion\";\n\ntype CinematicImageProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n};\n\n/**\n * Cinematic scale-DOWN settle (110% → 100%) for LARGE images only.\n * The wrapper (or a parent) MUST be `overflow-hidden` so the 1.1 scale is\n * clipped — otherwise the oversized image overflows and shifts layout.\n * Never apply this to titles/text; those stay a straight rise, no scale.\n */\nexport function CinematicImage({ children, className, style }: CinematicImageProps) {\n  return (\n    <m.div\n      className={className}\n      style={style}\n      initial={cinematicScale.initial}\n      whileInView={cinematicScale.animate}\n      viewport={viewportOnce}\n      transition={{ duration: durations.slow, ease: ease.out }}\n    >\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/cinematic-image.tsx"
    },
    {
      "path": "components/motion/clip-reveal.tsx",
      "content": "\"use client\";\n\nimport type { TargetAndTransition, Variants } from \"motion/react\";\nimport { useReducedMotion } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport {\n  fade,\n  revealTransition,\n  viewportOnce,\n  type WipeDirection,\n  wipeVariants,\n} from \"@/lib/motion\";\n\ntype ClipRevealProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  trigger?: \"inView\" | \"mount\";\n  direction?: WipeDirection;\n};\n\n/** Hard clip-path inset() edge wipe — the box stays put while its content is\n *  unveiled from `direction`. clip-path survives reducedMotion=\"user\", so we\n *  guard it: on reduce, the element renders fully open with a plain fade. */\nexport function ClipReveal({\n  children,\n  className,\n  style,\n  delay = 0,\n  trigger = \"inView\",\n  direction = \"up\",\n}: ClipRevealProps) {\n  const reduced = useReducedMotion();\n\n  // Reduced motion: clip-path is NOT auto-stripped, so drop the wipe entirely\n  // and settle in with a plain opacity fade (the prop-object `fade` carries no\n  // transition of its own, so the component-level `delay` applies cleanly).\n  if (reduced) {\n    const play =\n      trigger === \"mount\"\n        ? ({ animate: fade.animate } as const)\n        : ({ whileInView: fade.animate, viewport: viewportOnce } as const);\n    return (\n      <m.div\n        className={className}\n        style={style}\n        initial={fade.initial}\n        {...play}\n        transition={{ ...revealTransition, delay }}\n      >\n        {children}\n      </m.div>\n    );\n  }\n\n  // A variant's own transition overrides the component `transition` prop, so\n  // fold `delay` into the variant rather than passing it alongside.\n  const base = wipeVariants(direction) as {\n    initial: TargetAndTransition;\n    animate: TargetAndTransition;\n  };\n  const variants: Variants = {\n    initial: base.initial,\n    animate: { ...base.animate, transition: { ...revealTransition, delay } },\n  };\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: \"animate\" } as const)\n      : ({ whileInView: \"animate\", viewport: viewportOnce } as const);\n\n  return (\n    <m.div className={className} style={style} variants={variants} initial=\"initial\" {...play}>\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/clip-reveal.tsx"
    },
    {
      "path": "components/motion/fade.tsx",
      "content": "\"use client\";\n\nimport type { TargetAndTransition, Variants } from \"motion/react\";\nimport { useReducedMotion } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport {\n  type FadeDirection,\n  fadeVariants,\n  feel,\n  revealTransition,\n  viewportOnce,\n} from \"@/lib/motion\";\n\ntype FadeProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  /** Subtle FIXED drift as it fades — default \"up\". \"none\" = pure opacity. Tunable travel is Slide's job. */\n  direction?: FadeDirection;\n  /** Layer a blur → sharp focus-in on top. `true` uses feel.fadeBlur (~8px); a number sets custom px. */\n  blur?: boolean | number;\n  /** Animate the first time it's in view (once) or immediately on mount. */\n  trigger?: \"inView\" | \"mount\";\n};\n\n/**\n * The one prop-driven fade — absorbs the old FadeIn / Reveal / BlurToFocus / BlurFadeRise.\n * A refined opacity fade plus an optional subtle directional drift and an optional blur settle.\n * Transforms are stripped for free by the provider's `reducedMotion=\"user\"`, but `filter` blur\n * is NOT — so on reduced motion we drop both blur AND drift and keep a plain opacity fade.\n */\nexport function Fade({\n  children,\n  className,\n  style,\n  delay = 0,\n  direction = \"up\",\n  blur = false,\n  trigger = \"inView\",\n}: FadeProps) {\n  const reduceMotion = useReducedMotion();\n  const blurPx = blur === true ? feel.fadeBlur : blur === false ? 0 : blur;\n\n  // Reduced motion → a plain opacity fade, no drift, no filter. Otherwise the full recipe.\n  const base: Variants = reduceMotion\n    ? { initial: { opacity: 0 }, animate: { opacity: 1 } }\n    : fadeVariants(direction, blurPx);\n\n  // fadeVariants bakes no transition, so fold `delay` into `animate` here — a variant's own\n  // transition overrides the component-level `transition` prop (the L004 idiom, as SlideIn does).\n  const variants: Variants = {\n    ...base,\n    animate: {\n      ...(base.animate as TargetAndTransition),\n      transition: { ...revealTransition, delay },\n    },\n  };\n\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: \"animate\" } as const)\n      : ({ whileInView: \"animate\", viewport: viewportOnce } as const);\n\n  return (\n    <m.div className={className} style={style} variants={variants} initial=\"initial\" {...play}>\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/fade.tsx"
    },
    {
      "path": "components/motion/grayscale-reveal.tsx",
      "content": "\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { durations, ease, grayscaleReveal, viewportOnce } from \"@/lib/motion\";\n\ntype Props = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/**\n * Grayscale → color reveal, for images — `grayscale(1)` fades to `grayscale(0)`\n * alongside opacity, on `durations.slow` (filter animation reads best unhurried).\n *\n * `filter` isn't stripped by the reducedMotion=\"user\" provider (only transforms\n * are), so reduced-motion renders the final color state with a plain opacity\n * fade instead of animating the filter.\n */\nexport function GrayscaleReveal({\n  children,\n  className,\n  style,\n  delay = 0,\n  trigger = \"inView\",\n}: Props) {\n  const reduceMotion = useReducedMotion();\n  const transition = { duration: durations.slow, ease: ease.out, delay };\n  const target = reduceMotion ? { opacity: 1 } : grayscaleReveal.animate;\n  const initial = reduceMotion ? { opacity: 0 } : grayscaleReveal.initial;\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: target } as const)\n      : ({ whileInView: target, viewport: viewportOnce } as const);\n\n  return (\n    <m.div className={className} style={style} initial={initial} {...play} transition={transition}>\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/grayscale-reveal.tsx"
    },
    {
      "path": "components/motion/grow-from-origin.tsx",
      "content": "\"use client\";\n\nimport type { TargetAndTransition, Variants } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { growVariants, type Origin, revealTransition, viewportOnce } from \"@/lib/motion\";\n\ntype GrowFromOriginProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  /** transform-origin corner/center the element grows from. */\n  origin?: Origin;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/**\n * Grow from origin — fade + scale ~0.85 → 1, anchored to a transform-origin\n * corner/center. The dropdown/tooltip/popover entrance. Mounts by default\n * (menus don't wait on a scroll trigger); pass trigger=\"inView\" to scope it\n * to viewport entry instead. Transform-only — the reduced-motion provider\n * strips it for free, no manual guard needed.\n */\nexport function GrowFromOrigin({\n  children,\n  className,\n  style,\n  delay = 0,\n  origin = \"top\",\n  trigger = \"mount\",\n}: GrowFromOriginProps) {\n  const base = growVariants(origin);\n  // growVariants bakes revealTransition into `animate` itself — a delay has\n  // to be merged into that same target, since a component-level `transition`\n  // prop only applies as a fallback when the variant has none of its own.\n  const variants: Variants = {\n    ...base,\n    animate: {\n      ...(base.animate as TargetAndTransition),\n      transition: { ...revealTransition, delay },\n    },\n  };\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: \"animate\" } as const)\n      : ({ whileInView: \"animate\", viewport: viewportOnce } as const);\n\n  return (\n    <m.div className={className} style={style} variants={variants} initial=\"initial\" {...play}>\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/grow-from-origin.tsx"
    },
    {
      "path": "components/motion/inset-frame-reveal.tsx",
      "content": "\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { insetFrameReveal, revealTransition, viewportOnce } from \"@/lib/motion\";\n\ntype InsetFrameRevealProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/** Window settle — a uniform clip-path frame insets in from the edges, then opens to reveal. */\nexport function InsetFrameReveal({\n  children,\n  className,\n  style,\n  delay = 0,\n  trigger = \"inView\",\n}: InsetFrameRevealProps) {\n  const reducedMotion = useReducedMotion();\n\n  // `clip-path` isn't stripped by the provider's reducedMotion=\"user\" (transforms only),\n  // so when reduced motion is on, skip the frame entirely and render the settled state.\n  if (reducedMotion) {\n    return (\n      <div className={className} style={style}>\n        {children}\n      </div>\n    );\n  }\n\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: insetFrameReveal.animate } as const)\n      : ({ whileInView: insetFrameReveal.animate, viewport: viewportOnce } as const);\n\n  return (\n    <m.div\n      className={className}\n      style={style}\n      initial={insetFrameReveal.initial}\n      transition={{ ...revealTransition, delay }}\n      {...play}\n    >\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/inset-frame-reveal.tsx"
    },
    {
      "path": "components/motion/line-reveal.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport { type CSSProperties, isValidElement, type ReactElement, type ReactNode } from \"react\";\nimport { lineRevealContainer, lineRevealItem, viewportOnce } from \"@/lib/motion\";\n\nconst TAGS = {\n  span: m.span,\n  div: m.div,\n  p: m.p,\n  h1: m.h1,\n  h2: m.h2,\n  h3: m.h3,\n  h4: m.h4,\n  h5: m.h5,\n  h6: m.h6,\n} as const;\n\ntype Tag = keyof typeof TAGS;\ntype HostProps = { className?: string; style?: CSSProperties; children?: ReactNode };\n\ntype LineRevealProps = {\n  /** A single host element (h1–h6 / p / span / div) whose own children is a\n   *  plain string (split on \"\\n\"). LineReveal reconstructs THAT element as the\n   *  animated one — its tag, className + style win; no extra DOM wrapper. */\n  children: ReactNode;\n  delay?: number;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/** Per-line rise + fade, staggered and calm — for body copy / multiline blocks.\n *  Splits the wrapped element's string on \"\\n\"; each line clips inside its own\n *  overflow-hidden wrapper so the rise reads as an emerge, not a slide.\n *  Transform-only (provider handles reduced motion). aria-label carries the whole\n *  text so SR reads it as one passage. */\nexport function LineReveal({ children, delay = 0, trigger = \"inView\" }: LineRevealProps) {\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: \"animate\" } as const)\n      : ({ whileInView: \"animate\", viewport: viewportOnce } as const);\n\n  // Read the wrapped element's tag + props + string text so we can re-render it\n  // as the matching m[tag] carrying the stagger — the caller's tag/className win.\n  const child = isValidElement(children) ? (children as ReactElement<HostProps>) : null;\n  const tag = child && typeof child.type === \"string\" ? child.type : null;\n  const text = child && typeof child.props.children === \"string\" ? child.props.children : null;\n\n  // Fail soft: if the child isn't a single host element wrapping a plain string,\n  // render it untouched (unanimated) rather than crash.\n  if (!child || !tag || !(tag in TAGS) || text === null) {\n    return <>{children}</>;\n  }\n\n  const Container = TAGS[tag as Tag] as typeof m.div;\n  const { className, style } = child.props;\n  const lines = text\n    .split(\"\\n\")\n    .map((line) => line.trim())\n    .filter((line) => line.length > 0);\n  // Headings permit an accessible name; generic tags (span/div/p) don't — so an\n  // aria-label there is prohibited. role=\"img\" lets the label name the composite\n  // while the per-line masks stay aria-hidden.\n  const isHeading = tag.length === 2 && tag.startsWith(\"h\");\n\n  return (\n    <Container\n      className={className}\n      style={style}\n      aria-label={text}\n      role={isHeading ? undefined : \"img\"}\n      variants={lineRevealContainer(delay)}\n      initial=\"initial\"\n      {...play}\n    >\n      {lines.map((line, li) => {\n        const lineKey = `l${li}`;\n        return (\n          <span key={lineKey} aria-hidden style={{ display: \"block\", overflow: \"hidden\" }}>\n            <m.span variants={lineRevealItem} style={{ display: \"block\" }}>\n              {line}\n            </m.span>\n          </span>\n        );\n      })}\n    </Container>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/line-reveal.tsx"
    },
    {
      "path": "components/motion/mask-text-reveal.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport {\n  type CSSProperties,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { feel, maskTextItem, textRevealContainer, viewportOnce } from \"@/lib/motion\";\n\nconst TAGS = {\n  span: m.span,\n  div: m.div,\n  p: m.p,\n  h1: m.h1,\n  h2: m.h2,\n  h3: m.h3,\n  h4: m.h4,\n  h5: m.h5,\n  h6: m.h6,\n} as const;\n\ntype Tag = keyof typeof TAGS;\ntype HostProps = { className?: string; style?: CSSProperties; children?: ReactNode };\n\ntype MaskTextRevealProps = {\n  /** A single host element (h1–h6 / p / span / div) whose own children is a\n   *  plain string. MaskTextReveal reconstructs THAT element as the animated one —\n   *  its tag, className + style win; no extra DOM wrapper is added. */\n  children: ReactNode;\n  delay?: number;\n  by?: \"word\" | \"line\";\n  trigger?: \"inView\" | \"mount\";\n};\n\n/** Text slides up out of a hard mask — no fade, crisp editorial. Per token an\n *  overflow-hidden wrapper clips an inner span that rises from below (110% → 0). */\nexport function MaskTextReveal({\n  children,\n  delay = 0,\n  by = \"word\",\n  trigger = \"inView\",\n}: MaskTextRevealProps) {\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: \"animate\" } as const)\n      : ({ whileInView: \"animate\", viewport: viewportOnce } as const);\n\n  // Read the wrapped element's tag + props + string text so we can re-render it\n  // as the matching m[tag] carrying the stagger — the caller's tag/className win.\n  const child = isValidElement(children) ? (children as ReactElement<HostProps>) : null;\n  const tag = child && typeof child.type === \"string\" ? child.type : null;\n  const text = child && typeof child.props.children === \"string\" ? child.props.children : null;\n\n  // Fail soft: if the child isn't a single host element wrapping a plain string,\n  // render it untouched (unanimated) rather than crash.\n  if (!child || !tag || !(tag in TAGS) || text === null) {\n    return <>{children}</>;\n  }\n\n  const Container = TAGS[tag as Tag] as typeof m.span;\n  const { className, style } = child.props;\n  const step = by === \"line\" ? feel.lineStagger : feel.textStagger;\n  // Headings permit an accessible name; generic tags (span/div/p) don't — so an\n  // aria-label there is prohibited. role=\"img\" lets the label name the composite\n  // while the per-token masks stay aria-hidden.\n  const isHeading = tag.length === 2 && tag.startsWith(\"h\");\n  const tokens =\n    by === \"line\"\n      ? text\n          .split(\"\\n\")\n          .map((line) => line.trim())\n          .filter(Boolean)\n      : text.trim().split(/\\s+/);\n\n  return (\n    // aria-label carries the full string so SR reads it whole, not per-mask.\n    <Container\n      className={className}\n      style={style}\n      aria-label={text}\n      role={isHeading ? undefined : \"img\"}\n      variants={textRevealContainer(step, delay)}\n      initial=\"initial\"\n      {...play}\n    >\n      {tokens.map((token, ti) => {\n        const tokenKey = `t${ti}`;\n        return (\n          <Fragment key={tokenKey}>\n            <span\n              aria-hidden\n              style={{ display: by === \"line\" ? \"block\" : \"inline-block\", overflow: \"hidden\" }}\n            >\n              <m.span variants={maskTextItem} style={{ display: \"inline-block\" }}>\n                {token}\n              </m.span>\n            </span>\n            {by === \"word\" && ti < tokens.length - 1 ? \" \" : null}\n          </Fragment>\n        );\n      })}\n    </Container>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/mask-text-reveal.tsx"
    },
    {
      "path": "components/motion/perspective-tilt-in.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { perspectiveTilt, revealTransition, viewportOnce } from \"@/lib/motion\";\n\ntype PerspectiveTiltInProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/**\n * Reveal + a whisper of depth — rise + rotateX settle over a perspective,\n * one straight ease-out, no spring. Transform-only (translate/rotate), so\n * the app-level `reducedMotion=\"user\"` provider strips it automatically —\n * no manual guard needed here.\n */\nexport function PerspectiveTiltIn({\n  children,\n  className,\n  style,\n  delay = 0,\n  trigger = \"inView\",\n}: PerspectiveTiltInProps) {\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: perspectiveTilt.animate } as const)\n      : ({ whileInView: perspectiveTilt.animate, viewport: viewportOnce } as const);\n\n  return (\n    <m.div\n      className={className}\n      style={style}\n      initial={perspectiveTilt.initial}\n      {...play}\n      transition={{ ...revealTransition, delay }}\n    >\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/perspective-tilt-in.tsx"
    },
    {
      "path": "components/motion/radial-stagger.tsx",
      "content": "\"use client\";\n\nimport type { Variants } from \"motion/react\";\nimport { useAnimationControls } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { Children, useEffect, useState } from \"react\";\nimport { feel, springPop, springPopTransition } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype DistanceMetric = \"euclidean\" | \"manhattan\" | \"chebyshev\";\n\ntype RadialStaggerProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  /** classes for each tile (the grid cell) */\n  itemClassName?: string;\n  /** grid columns — drives both the layout and the distance geometry */\n  columns?: number;\n  /** seconds of delay added per unit of distance from the clicked origin */\n  step?: number;\n  /** how ring distance is measured from the origin cell */\n  distance?: DistanceMetric;\n  /** index the first cascade ripples out from (before any click) */\n  defaultOrigin?: number;\n  /**\n   * Opt into real interactive controls — each tile renders as a focusable\n   * `<button>`. Requires an accessible name per tile (see `getItemLabel`),\n   * otherwise you ship nameless buttons (WCAG 4.1.2). Leave off (default) for\n   * a decorative ripple grid: tiles render as non-focusable `role=\"presentation\"`\n   * elements, so keyboard/SR users never hit empty controls.\n   */\n  interactive?: boolean;\n  /** Accessible name for the tile at `index` — required when `interactive`. */\n  getItemLabel?: (index: number) => string;\n  /** Fires with the tile index when an interactive tile is selected. */\n  onSelect?: (index: number) => void;\n};\n\nconst radialItem: Variants = {\n  hidden: springPop.initial,\n  visible: (delay: number) => ({\n    ...springPop.animate,\n    transition: { ...springPopTransition, delay },\n  }),\n};\n\nfunction ringDistance(a: number, b: number, columns: number, metric: DistanceMetric) {\n  const cols = Math.max(1, columns);\n  const dr = Math.abs(Math.floor(a / cols) - Math.floor(b / cols));\n  const dc = Math.abs((a % cols) - (b % cols));\n  if (metric === \"manhattan\") return dr + dc;\n  if (metric === \"chebyshev\") return Math.max(dr, dc);\n  return Math.hypot(dr, dc);\n}\n\n// FLAT export (never a compound `RadialStagger.Item` — bolted-on props are\n// stripped across the RSC client boundary and crash a Server Component at build).\nexport function RadialStagger({\n  children,\n  className,\n  style,\n  itemClassName,\n  columns = 4,\n  step = feel.stagger,\n  distance = \"euclidean\",\n  defaultOrigin = 0,\n  interactive = false,\n  getItemLabel,\n  onSelect,\n}: RadialStaggerProps) {\n  const [origin, setOrigin] = useState(defaultOrigin);\n  const [nonce, setNonce] = useState(0);\n  const controls = useAnimationControls();\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: origin/nonce are the replay trigger — re-running after the fresh `custom` delays commit is the point\n  useEffect(() => {\n    controls.set(\"hidden\");\n    controls.start(\"visible\");\n  }, [origin, nonce, controls]);\n\n  function ripple(index: number) {\n    setOrigin(index);\n    setNonce((n) => n + 1);\n  }\n\n  return (\n    <div\n      className={className}\n      style={{\n        display: \"grid\",\n        gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))`,\n        ...style,\n      }}\n    >\n      {Children.map(children, (child, index) => {\n        const motionProps = {\n          custom: ringDistance(index, origin, columns, distance) * step,\n          variants: radialItem,\n          initial: \"hidden\" as const,\n          animate: controls,\n        };\n\n        // Opt-in interactive: real focusable button, must be labelled.\n        if (interactive) {\n          return (\n            <m.button\n              {...motionProps}\n              type=\"button\"\n              aria-label={getItemLabel?.(index)}\n              onClick={() => {\n                ripple(index);\n                onSelect?.(index);\n              }}\n              className={cn(\n                \"cursor-pointer appearance-none border-0 bg-transparent p-0 text-left focus-visible:outline-2 focus-visible:outline-offset-2\",\n                itemClassName,\n              )}\n            >\n              {child}\n            </m.button>\n          );\n        }\n\n        // Default decorative: non-focusable, no accessible name needed. The\n        // click-to-ripple-outward effect still works — clicking a tile cascades\n        // ripples from it — it just isn't exposed to keyboard/SR as a control.\n        return (\n          <m.div\n            {...motionProps}\n            role=\"presentation\"\n            onClick={() => ripple(index)}\n            className={cn(\"cursor-pointer\", itemClassName)}\n          >\n            {child}\n          </m.div>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/radial-stagger.tsx"
    },
    {
      "path": "components/motion/scale-in.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { revealTransition, scaleIn, viewportOnce } from \"@/lib/motion\";\n\ntype ScaleInProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/** Soft 0.95 → 1 grow + fade — a straight ease tween, never a spring. */\nexport function ScaleIn({\n  children,\n  className,\n  style,\n  delay = 0,\n  trigger = \"inView\",\n}: ScaleInProps) {\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: scaleIn.animate } as const)\n      : ({ whileInView: scaleIn.animate, viewport: viewportOnce } as const);\n\n  return (\n    <m.div\n      className={className}\n      style={style}\n      initial={scaleIn.initial}\n      {...play}\n      transition={{ ...revealTransition, delay }}\n    >\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/scale-in.tsx"
    },
    {
      "path": "components/motion/scale-x-reveal.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { revealTransition, scaleXReveal, viewportOnce } from \"@/lib/motion\";\n\ntype Props = {\n  children?: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/**\n * A rule / underline / divider draws open — scaleX 0 → 1 + fade from the left.\n * Self-contained by default: `<ScaleXReveal className=\"h-px w-full bg-border\" />`.\n * If you do pass text children, put it in an inner un-scaled span — this\n * component scales the whole element horizontally, which would otherwise\n * squash the text as it draws in.\n */\nexport function ScaleXReveal({ children, className, style, delay = 0, trigger = \"inView\" }: Props) {\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: scaleXReveal.animate } as const)\n      : ({ whileInView: scaleXReveal.animate, viewport: viewportOnce } as const);\n\n  return (\n    <m.div\n      className={className}\n      style={{ transformOrigin: \"left\", ...style }}\n      initial={scaleXReveal.initial}\n      {...play}\n      transition={{ ...revealTransition, delay }}\n    >\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/scale-x-reveal.tsx"
    },
    {
      "path": "components/motion/sibling-dimming.tsx",
      "content": "import type { ComponentPropsWithoutRef, CSSProperties } from \"react\";\nimport { feel } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\n// Container: when it HAS a hovered item, every non-hovered item dims to 50%.\n// `:not(:hover)` keeps the hovered item untouched, so there's no self-hover\n// specificity race. Pair with SiblingDimmingItem (adds the `.dim-item` marker).\nexport function SiblingDimming({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      className={cn(\"[&:has(.dim-item:hover)_.dim-item:not(:hover)]:opacity-50\", className)}\n      {...props}\n    />\n  );\n}\n\nconst dimTransition: CSSProperties = {\n  transitionDuration: `${feel.duration.fast}s`,\n  transitionTimingFunction: `cubic-bezier(${feel.ease.join(\",\")})`,\n};\n\nexport function SiblingDimmingItem({\n  className,\n  style,\n  ...props\n}: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      className={cn(\"dim-item transition-opacity motion-reduce:transition-none\", className)}\n      style={{ ...dimTransition, ...style }}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/sibling-dimming.tsx"
    },
    {
      "path": "components/motion/slide.tsx",
      "content": "\"use client\";\n\nimport type { TargetAndTransition, Variants } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport {\n  revealTransition,\n  type SlideDirection,\n  slideDistance,\n  slideVariants,\n  viewportOnce,\n} from \"@/lib/motion\";\n\ntype DistanceTier = keyof typeof slideDistance;\n\ntype SlideProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n  direction: SlideDirection;\n  distance?: DistanceTier | number;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/** Directional fade + slide — enters from one of 8 travel directions. Transform-only, so the provider's `reducedMotion=\"user\"` strips it for free. */\nexport function Slide({\n  children,\n  className,\n  style,\n  delay = 0,\n  direction,\n  distance = \"base\",\n  trigger = \"inView\",\n}: SlideProps) {\n  const px = typeof distance === \"number\" ? distance : slideDistance[distance];\n  const base = slideVariants(direction, px);\n  // slideVariants bakes revealTransition into `animate`, and a variant's own\n  // transition overrides the component-level `transition` prop — so fold `delay`\n  // into that same target (same idiom as ClipReveal / GrowFromOrigin).\n  const variants: Variants = {\n    ...base,\n    animate: {\n      ...(base.animate as TargetAndTransition),\n      transition: { ...revealTransition, delay },\n    },\n  };\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: \"animate\" } as const)\n      : ({ whileInView: \"animate\", viewport: viewportOnce } as const);\n\n  return (\n    <m.div className={className} style={style} variants={variants} initial=\"initial\" {...play}>\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/slide.tsx"
    },
    {
      "path": "components/motion/spring-pop.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { springPop, springPopTransition } from \"@/lib/motion\";\n\ntype SpringPopProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  delay?: number;\n};\n\n// The ~1.05 overshoot emerges from the spring itself — never keyframe it.\n// Reduced motion drops the scale (a transform), leaving a clean opacity fade.\nexport function SpringPop({ children, className, style, delay = 0 }: SpringPopProps) {\n  return (\n    <m.div\n      className={className}\n      style={style}\n      initial={springPop.initial}\n      animate={springPop.animate}\n      transition={{ ...springPopTransition, delay }}\n    >\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/spring-pop.tsx"
    },
    {
      "path": "components/motion/stagger.tsx",
      "content": "\"use client\";\n\nimport type { Variants } from \"motion/react\";\nimport * as m from \"motion/react-m\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { feel, staggerItem, viewportOnce } from \"@/lib/motion\";\n\ntype StaggerProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  /** seconds between each child (LinearStagger — tunable step) */\n  staggerChildren?: number;\n  /** seconds before the first child */\n  delayChildren?: number;\n};\n\ntype StaggerItemProps = {\n  children: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n};\n\n/**\n * Staggered children reveal. FLAT exports on purpose — never a compound\n * `Stagger.Item` (a bolted-on property is undefined across the RSC\n * client-reference boundary and crashes a Server Component at build).\n */\nexport function Stagger({\n  children,\n  className,\n  style,\n  staggerChildren = feel.stagger,\n  delayChildren = feel.staggerDelay,\n}: StaggerProps) {\n  // Built from props so the step is tunable; defaults reproduce staggerContainer exactly.\n  const container: Variants = {\n    initial: {},\n    animate: { transition: { staggerChildren, delayChildren } },\n  };\n  return (\n    <m.div\n      className={className}\n      style={style}\n      variants={container}\n      initial=\"initial\"\n      whileInView=\"animate\"\n      viewport={viewportOnce}\n    >\n      {children}\n    </m.div>\n  );\n}\n\nexport function StaggerItem({ children, className, style }: StaggerItemProps) {\n  return (\n    <m.div className={className} style={style} variants={staggerItem}>\n      {children}\n    </m.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/stagger.tsx"
    },
    {
      "path": "components/motion/text-reveal.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport {\n  type CSSProperties,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { feel, textRevealContainer, textRevealItem, viewportOnce } from \"@/lib/motion\";\n\nconst TAGS = {\n  span: m.span,\n  div: m.div,\n  p: m.p,\n  h1: m.h1,\n  h2: m.h2,\n  h3: m.h3,\n  h4: m.h4,\n  h5: m.h5,\n  h6: m.h6,\n} as const;\n\ntype Tag = keyof typeof TAGS;\ntype HostProps = { className?: string; style?: CSSProperties; children?: ReactNode };\n\ntype TextRevealProps = {\n  /** A single host element (h1–h6 / p / span / div) whose own children is a\n   *  plain string. TextReveal reconstructs THAT element as the animated one —\n   *  its tag, className + style win; no extra DOM wrapper is added. */\n  children: ReactNode;\n  delay?: number;\n  by?: \"word\" | \"char\";\n  stagger?: number;\n  trigger?: \"inView\" | \"mount\";\n};\n\nexport function TextReveal({\n  children,\n  delay = 0,\n  by = \"word\",\n  stagger,\n  trigger = \"inView\",\n}: TextRevealProps) {\n  const step = stagger ?? (by === \"char\" ? feel.textStaggerChar : feel.textStagger);\n  const play =\n    trigger === \"mount\"\n      ? ({ animate: \"animate\" } as const)\n      : ({ whileInView: \"animate\", viewport: viewportOnce } as const);\n\n  // Read the wrapped element's tag + props + string text so we can re-render it\n  // as the matching m[tag] carrying the stagger — the caller's tag/className win.\n  const child = isValidElement(children) ? (children as ReactElement<HostProps>) : null;\n  const tag = child && typeof child.type === \"string\" ? child.type : null;\n  const text = child && typeof child.props.children === \"string\" ? child.props.children : null;\n\n  // Fail soft: if the child isn't a single host element wrapping a plain string,\n  // render it untouched (unanimated) rather than crash.\n  if (!child || !tag || !(tag in TAGS) || text === null) {\n    return <>{children}</>;\n  }\n\n  const Container = TAGS[tag as Tag] as typeof m.span;\n  const { className, style } = child.props;\n  const words = text.trim().split(/\\s+/);\n  // Headings permit an accessible name; generic tags (span/div/p) don't — so an\n  // aria-label there is prohibited. role=\"img\" lets the label name the composite\n  // while the per-token spans stay aria-hidden (no char-by-char read).\n  const isHeading = tag.length === 2 && tag.startsWith(\"h\");\n\n  return (\n    // aria-label carries the full string so SR reads it whole, not per-span.\n    <Container\n      className={className}\n      style={style}\n      aria-label={text}\n      role={isHeading ? undefined : \"img\"}\n      variants={textRevealContainer(step, delay)}\n      initial=\"initial\"\n      {...play}\n    >\n      {words.map((word, wi) => {\n        const wordKey = `w${wi}`;\n        return (\n          <Fragment key={wordKey}>\n            {by === \"word\" ? (\n              <m.span aria-hidden variants={textRevealItem} style={{ display: \"inline-block\" }}>\n                {word}\n              </m.span>\n            ) : (\n              <span aria-hidden style={{ display: \"inline-block\" }}>\n                {Array.from(word).map((ch, ci) => {\n                  const charKey = `${wordKey}c${ci}`;\n                  return (\n                    <m.span\n                      key={charKey}\n                      variants={textRevealItem}\n                      style={{ display: \"inline-block\" }}\n                    >\n                      {ch}\n                    </m.span>\n                  );\n                })}\n              </span>\n            )}\n            {wi < words.length - 1 ? \" \" : null}\n          </Fragment>\n        );\n      })}\n    </Container>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/text-reveal.tsx"
    }
  ],
  "docs": "Mount <MotionProvider> once in your root layout, then wrap anything in <Fade> / <Stagger> / <Slide> / etc. Retune the whole engine from the `feel` block at the top of lib/motion.ts. Primitives import `m` from motion/react-m — a raw motion.* throws under strict LazyMotion.",
  "type": "registry:block"
}