{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reveal",
  "type": "registry:ui",
  "dependencies": [
    "motion",
    "@usva-ui/tokens"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "reveal.tsx",
      "target": "components/ui/reveal.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport {\n  motion,\n  useReducedMotion,\n  useScroll,\n  useTransform,\n} from \"motion/react\";\nimport * as React from \"react\";\nimport { buildReveal, type RevealVariant, scrubRanges } from \"./presets\";\nimport { useRevealIntensity } from \"./reveal-config\";\n\nconst MOTION_TAGS = {\n  div: motion.div,\n  section: motion.section,\n  article: motion.article,\n  h2: motion.h2,\n  h3: motion.h3,\n  p: motion.p,\n  li: motion.li,\n  ul: motion.ul,\n  span: motion.span,\n  figure: motion.figure,\n} as const;\n\nexport type RevealTag = keyof typeof MOTION_TAGS;\n\nconst SCRUB_OFFSET: [string, string] = [\"start 0.88\", \"start 0.45\"];\n\nconst useIsoLayoutEffect =\n  typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nfunction assignRef<T>(ref: React.Ref<T> | undefined, node: T | null) {\n  if (typeof ref === \"function\") ref(node);\n  else if (ref) (ref as React.MutableRefObject<T | null>).current = node;\n}\n\n/**\n * Decide, before paint, whether an element should reveal. Elements already in\n * view at mount stay static (no hide-flash, no blank on no-JS); only ones below\n * the fold arm the enter animation.\n */\nfunction useArmed(amount: number, disabled: boolean, force: boolean) {\n  const ref = React.useRef<HTMLElement | null>(null);\n  const [armed, setArmed] = React.useState(force && !disabled);\n  useIsoLayoutEffect(() => {\n    if (disabled || force) return;\n    const el = ref.current;\n    if (!el) return;\n    const rect = el.getBoundingClientRect();\n    if (rect.top > window.innerHeight * (1 - amount)) setArmed(true);\n  }, [disabled, amount, force]);\n  return { ref, armed };\n}\n\nexport interface RevealProps extends React.HTMLAttributes<HTMLElement> {\n  variant?: RevealVariant;\n  delay?: number;\n  /** Override the ambient reveal intensity for this element. */\n  intensity?: number;\n  /** Fraction of the element that must be visible to trigger. */\n  amount?: number;\n  /** Reveal even when already in view at mount (e.g. demos, explicit entrances). */\n  force?: boolean;\n  /**\n   * Drive the variant from scroll progress instead of a threshold crossing. The\n   * reveal becomes a continuous function of position: it plays backwards on\n   * scroll-up and holds mid-state when the reader stops. Opt-in, because a\n   * threshold reveal is still the right answer for anything that encodes a\n   * value (a bar animating to 71% must reach 71%, not to wherever the scroll is).\n   */\n  scrub?: boolean;\n  /**\n   * Scroll window the scrub spans, as `motion`'s `useScroll` offset. Defaults to\n   * starting when the element's top reaches 88% of the viewport and completing\n   * at 45%, so it resolves on approach and settles before the reader arrives.\n   */\n  scrubOffset?: [string, string];\n  as?: RevealTag;\n}\n\nexport const Reveal = React.forwardRef<HTMLElement, RevealProps>(\n  (\n    {\n      variant = \"veil\",\n      delay = 0,\n      intensity,\n      amount = 0.35,\n      force = false,\n      scrub = false,\n      scrubOffset = SCRUB_OFFSET,\n      as = \"div\",\n      children,\n      ...rest\n    },\n    forwardedRef,\n  ) => {\n    const k = useRevealIntensity(intensity);\n    const reduced = useReducedMotion() ?? false;\n    const { ref, armed } = useArmed(amount, reduced || k <= 0, force);\n\n    const setRefs = React.useCallback(\n      (node: HTMLElement | null) => {\n        ref.current = node;\n        assignRef(forwardedRef, node);\n      },\n      [ref, forwardedRef],\n    );\n\n    const Comp = MOTION_TAGS[as] as React.ElementType;\n\n    const scrubbing = scrub && !reduced && k > 0;\n    const ranges = scrubRanges(buildReveal(variant, k, reduced));\n    const { scrollYProgress } = useScroll({\n      target: ref as React.RefObject<HTMLElement>,\n      offset: scrubOffset as never,\n    });\n    const scrubOpacity = useTransform(scrollYProgress, [0, 1], ranges.opacity);\n    const scrubX = useTransform(scrollYProgress, [0, 1], ranges.x);\n    const scrubY = useTransform(scrollYProgress, [0, 1], ranges.y);\n    const scrubScale = useTransform(scrollYProgress, [0, 1], ranges.scale);\n    const scrubBlur = useTransform(scrollYProgress, (v: number) => {\n      const [from, to] = ranges.blur;\n      return `blur(${(from + (to - from) * v).toFixed(2)}px)`;\n    });\n\n    if (scrubbing) {\n      const { present } = ranges;\n      return (\n        <Comp\n          ref={setRefs}\n          style={{\n            opacity: scrubOpacity,\n            ...(present.x ? { x: scrubX } : {}),\n            ...(present.y ? { y: scrubY } : {}),\n            ...(present.scale ? { scale: scrubScale } : {}),\n            ...(present.blur ? { filter: scrubBlur } : {}),\n          }}\n          {...rest}\n        >\n          {children}\n        </Comp>\n      );\n    }\n\n    if (!armed) {\n      return (\n        <Comp ref={setRefs} {...rest}>\n          {children}\n        </Comp>\n      );\n    }\n\n    const built = buildReveal(variant, k, reduced);\n    return (\n      <Comp\n        ref={setRefs}\n        initial={built.initial}\n        whileInView={built.animate}\n        viewport={{ once: true, amount }}\n        transition={{ ...built.transition, delay }}\n        {...rest}\n      >\n        {children}\n      </Comp>\n    );\n  },\n);\nReveal.displayName = \"Reveal\";\n\nexport interface RevealGroupProps extends React.HTMLAttributes<HTMLElement> {\n  variant?: RevealVariant;\n  /** Delay between each child, seconds. */\n  stagger?: number;\n  /** Delay before the first child, seconds. */\n  delay?: number;\n  intensity?: number;\n  amount?: number;\n  /** Reveal even when already in view at mount (e.g. demos). */\n  force?: boolean;\n  as?: RevealTag;\n}\n\n/**\n * Staggers its direct children in on one shared viewport trigger. The cascade\n * IS the effect. Each child becomes an animated box, so put layout classes\n * (grid/flex) on the group.\n */\nexport const RevealGroup = React.forwardRef<HTMLElement, RevealGroupProps>(\n  (\n    {\n      variant = \"tick\",\n      stagger = 0.06,\n      delay = 0,\n      intensity,\n      amount = 0.3,\n      force = false,\n      as = \"div\",\n      children,\n      ...rest\n    },\n    forwardedRef,\n  ) => {\n    const k = useRevealIntensity(intensity);\n    const reduced = useReducedMotion() ?? false;\n    const disabled = reduced || k <= 0;\n    const { ref, armed } = useArmed(amount, disabled, force);\n\n    const setRefs = React.useCallback(\n      (node: HTMLElement | null) => {\n        ref.current = node;\n        assignRef(forwardedRef, node);\n      },\n      [ref, forwardedRef],\n    );\n\n    const Comp = MOTION_TAGS[as] as React.ElementType;\n\n    if (!armed) {\n      return (\n        <Comp ref={setRefs} {...rest}>\n          {children}\n        </Comp>\n      );\n    }\n\n    const built = buildReveal(variant, k, reduced);\n    const childVariants = {\n      hidden: built.initial,\n      show: { ...built.animate, transition: built.transition },\n    };\n\n    return (\n      <Comp\n        ref={setRefs}\n        initial=\"hidden\"\n        whileInView=\"show\"\n        viewport={{ once: true, amount }}\n        variants={{\n          hidden: {},\n          show: {\n            transition: { staggerChildren: stagger, delayChildren: delay },\n          },\n        }}\n        {...rest}\n      >\n        {React.Children.map(children, (child) => (\n          <motion.div variants={childVariants}>{child}</motion.div>\n        ))}\n      </Comp>\n    );\n  },\n);\nRevealGroup.displayName = \"RevealGroup\";\n"
    },
    {
      "path": "reveal-config.tsx",
      "target": "components/ui/reveal-config.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport * as React from \"react\";\n\nexport interface RevealConfig {\n  /** Global reveal intensity: kajo = 1 (bold), sisu ≈ 0.45 (quiet), 0 = crossfade. */\n  intensity: number;\n}\n\nconst RevealConfigContext = React.createContext<RevealConfig>({ intensity: 1 });\n\nexport function RevealConfigProvider({\n  intensity,\n  children,\n}: {\n  intensity: number;\n  children: React.ReactNode;\n}) {\n  const value = React.useMemo(() => ({ intensity }), [intensity]);\n  return (\n    <RevealConfigContext.Provider value={value}>\n      {children}\n    </RevealConfigContext.Provider>\n  );\n}\n\nexport function useRevealIntensity(override?: number): number {\n  const ctx = React.useContext(RevealConfigContext);\n  return override ?? ctx.intensity;\n}\n"
    },
    {
      "path": "presets.ts",
      "target": "components/ui/presets.ts",
      "type": "registry:ui",
      "content": "import { tokens } from \"@usva-ui/tokens\";\n\nexport const springs = tokens.motion.spring;\n\n/**\n * Legacy variant objects (framer/motion `variants` shape). Kept for consumers\n * that drive their own `motion` components; the Reveal system below supersedes\n * this for scroll reveals.\n */\nexport const variants = {\n  fadeUp: {\n    hidden: { opacity: 0, y: 8 },\n    show: { opacity: 1, y: 0, transition: springs.soft },\n  },\n  stagger: {\n    hidden: {},\n    show: { transition: { staggerChildren: 0.06 } },\n  },\n} as const;\n\n/** Tactile press feedback. Spread onto a `motion` component. Never below 0.96. */\nexport const press = {\n  whileTap: { scale: 0.96 },\n  transition: springs.soft,\n} as const;\n\n// ── Reveal system: \"resolve from mist\" ──────────────────────────────\n// Six distinct reveals, one metaphor: things resolve out of fog toward the\n// light above. Assign by content role, never by position. One `intensity`\n// scalar dials the whole set from kajo-bold (1) to sisu-quiet (~0.45) to a\n// plain crossfade (0, also the reduced-motion path).\n\nconst EASE = {\n  quart: [0.25, 1, 0.5, 1],\n  quint: [0.22, 1, 0.36, 1],\n  expo: [0.16, 1, 0.3, 1],\n} as const;\n\nexport type RevealVariant =\n  | \"veil\"\n  | \"cast\"\n  | \"surface\"\n  | \"focus\"\n  | \"tick\"\n  | \"lean\";\n\ntype RevealSpec = {\n  x?: number;\n  y?: number;\n  scale?: number;\n  blur?: number;\n  transition: Record<string, unknown>;\n};\n\nconst SPECS: Record<RevealVariant, RevealSpec> = {\n  // default; mist thinning: prose, generic sections, footers\n  veil: { y: 12, blur: 3, transition: { duration: 0.5, ease: EASE.quart } },\n  // light resolving from above: headings, eyebrows, titles (moves DOWN)\n  cast: { y: -10, blur: 8, transition: { duration: 0.7, ease: EASE.quint } },\n  // material rising to the light: cards, panels, CTAs, hero (spring, no blur)\n  surface: { y: 20, scale: 0.97, transition: springs.soft },\n  // lens finding focus: images, media frames (no travel; clip the scale)\n  focus: {\n    scale: 1.04,\n    blur: 12,\n    transition: { duration: 0.9, ease: EASE.expo },\n  },\n  // instrument reading: stats, tables, mono/tabular (never blurs, grouped)\n  tick: { y: 6, transition: { duration: 0.28, ease: EASE.expo } },\n  // aside voice: quotes, testimonials, callouts (the only horizontal move)\n  lean: { x: -16, transition: { duration: 0.6, ease: EASE.quint } },\n};\n\nexport interface BuiltReveal {\n  initial: Record<string, number | string>;\n  animate: Record<string, number | string>;\n  transition: Record<string, unknown>;\n}\n\nexport interface ScrubRanges {\n  opacity: [number, number];\n  x: [number, number];\n  y: [number, number];\n  scale: [number, number];\n  blur: [number, number];\n  present: { x: boolean; y: boolean; scale: boolean; blur: boolean };\n}\n\nconst BLUR_PX = /blur\\(([\\d.]+)px\\)/;\n\nfunction px(value: number | string | undefined): number {\n  if (typeof value === \"number\") return value;\n  if (typeof value === \"string\") {\n    const hit = BLUR_PX.exec(value);\n    if (hit?.[1]) return Number.parseFloat(hit[1]);\n  }\n  return 0;\n}\n\nexport function scrubRanges(built: BuiltReveal): ScrubRanges {\n  const { initial, animate } = built;\n  const pair = (key: string, fallback: number): [number, number] => [\n    typeof initial[key] === \"number\" ? (initial[key] as number) : fallback,\n    typeof animate[key] === \"number\" ? (animate[key] as number) : fallback,\n  ];\n\n  return {\n    opacity: pair(\"opacity\", 1),\n    x: pair(\"x\", 0),\n    y: pair(\"y\", 0),\n    scale: pair(\"scale\", 1),\n    blur: [px(initial.filter), px(animate.filter)],\n    present: {\n      x: initial.x != null,\n      y: initial.y != null,\n      scale: initial.scale != null,\n      blur: initial.filter != null,\n    },\n  };\n}\n\n/**\n * Compute a variant's motion objects, scaled by intensity `k` (0..1). Blur under\n * 2px is dropped rather than rendered, so a low `k` degrades to a plain move.\n */\nexport function buildReveal(\n  variant: RevealVariant,\n  k: number,\n  reduced: boolean,\n): BuiltReveal {\n  if (reduced || k <= 0) {\n    return {\n      initial: { opacity: 0 },\n      animate: { opacity: 1 },\n      transition: { duration: variant === \"tick\" ? 0 : 0.15, ease: \"linear\" },\n    };\n  }\n\n  const spec = SPECS[variant];\n  const initial: Record<string, number | string> = { opacity: 0 };\n  const animate: Record<string, number | string> = { opacity: 1 };\n\n  const x = (spec.x ?? 0) * k;\n  const y = (spec.y ?? 0) * k;\n  if (x) {\n    initial.x = x;\n    animate.x = 0;\n  }\n  if (y) {\n    initial.y = y;\n    animate.y = 0;\n  }\n  if (spec.scale != null) {\n    initial.scale = 1 + (spec.scale - 1) * k;\n    animate.scale = 1;\n  }\n  const blur = (spec.blur ?? 0) * k;\n  if (blur >= 2) {\n    initial.filter = `blur(${blur.toFixed(1)}px)`;\n    animate.filter = \"blur(0px)\";\n  }\n\n  const isSpring = spec.transition.type === \"spring\";\n  const transition = isSpring\n    ? spec.transition\n    : {\n        ...spec.transition,\n        duration: (spec.transition.duration as number) * (0.7 + 0.3 * k),\n      };\n\n  return { initial, animate, transition };\n}\n"
    }
  ]
}