{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gradient-reveal-text",
  "type": "registry:ui",
  "title": "Gradient Reveal Text",
  "description": "A paragraph whose words take on an accent gradient as you scroll it into place.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/gradient-reveal-text.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useScroll, useSpring, useTransform, type MotionValue } from \"motion/react\";\n\n/* SSR-safe: renders \"no preference\" on the server and the first client pass, so markup always hydrates. */\nfunction subscribeReducedMotion(onChange: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", onChange);\n  return () => mq.removeEventListener(\"change\", onChange);\n}\n\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\ntype ScrollOffset = NonNullable<NonNullable<Parameters<typeof useScroll>[0]>[\"offset\"]>;\n\nexport interface GradientRevealTextProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  /** The paragraph text. */\n  children: string;\n  /** Element to render. */\n  as?: \"p\" | \"h2\" | \"h3\" | \"blockquote\" | \"div\";\n  /** Scrollable ancestor to track. Defaults to the window. */\n  container?: React.RefObject<HTMLElement | null>;\n  /** Reveal words from first to last, or from last to first. */\n  direction?: \"forward\" | \"reverse\";\n  /** Gradient start colour (any CSS colour). */\n  accent?: string;\n  /** Gradient end colour (any CSS colour). Deepened automatically on light backgrounds. */\n  accentSecondary?: string;\n  /** Opacity of words that have not been revealed yet (0–1). */\n  dimOpacity?: number;\n  /** How many words are mid-transition at once — higher is softer. */\n  softness?: number;\n  /** Smooth scroll progress with a light spring. */\n  smooth?: boolean;\n  /** Scroll offsets, as in motion's useScroll: when the reveal starts and ends. */\n  offset?: ScrollOffset;\n}\n\nconst DEFAULT_OFFSET: ScrollOffset = [\"start 0.85\", \"end 0.45\"];\n\n/** Mixes the two accents for word `index` of `total`, so the paragraph reads as one gradient. */\nfunction wordColor(index: number, total: number) {\n  const share = total <= 1 ? 100 : Math.round((1 - index / (total - 1)) * 100);\n  return `color-mix(in oklab, var(--grt-a) ${share}%, var(--grt-b))`;\n}\n\ninterface WordProps {\n  word: string;\n  color: string;\n  progress: MotionValue<number>;\n  /** Position in reveal order (0 = first revealed). */\n  order: number;\n  total: number;\n  softness: number;\n}\n\nfunction RevealWord({ word, color, progress, order, total, softness }: WordProps) {\n  // Spread the scroll range over the words; each takes `softness` words' worth of progress to fade in.\n  const reveal = useTransform(progress, (p) =>\n    Math.min(1, Math.max(0, (p * (total + softness) - order) / softness)),\n  );\n  // The dim base fades out as the accent layer fades in, so the two never muddy together.\n  const base = useTransform(reveal, (r) => 1 - r);\n\n  return (\n    <span className=\"relative inline-block\">\n      <motion.span className=\"text-zinc-900 dark:text-zinc-100\" style={{ opacity: base }}>\n        <span className=\"opacity-[var(--grt-dim)]\">{word}</span>\n      </motion.span>\n      <motion.span className=\"absolute inset-0\" style={{ opacity: reveal, color }}>\n        {word}\n      </motion.span>\n    </span>\n  );\n}\n\n/**\n * A paragraph whose words take on an accent gradient as it scrolls through the viewport\n * (or through any scrollable container passed via `container`).\n */\nexport function GradientRevealText({\n  children,\n  as = \"p\",\n  container,\n  direction = \"forward\",\n  accent = \"#FF7448\",\n  accentSecondary = \"#B6F3D1\",\n  dimOpacity = 0.18,\n  softness = 3,\n  smooth = true,\n  offset = DEFAULT_OFFSET,\n  className = \"\",\n  style,\n  ...props\n}: GradientRevealTextProps) {\n  const reduceMotion = usePrefersReducedMotion();\n  const ref = React.useRef<HTMLElement>(null);\n  const { scrollYProgress } = useScroll({ target: ref, container, offset });\n  const smoothed = useSpring(scrollYProgress, { stiffness: 160, damping: 30, mass: 0.4, restDelta: 0.0005 });\n  const progress = smooth ? smoothed : scrollYProgress;\n  const Tag = as as React.ElementType;\n\n  const words = children.split(/\\s+/).filter(Boolean);\n  const total = words.length;\n\n  return (\n    <Tag\n      ref={ref}\n      className={\n        // On light backgrounds the accents are deepened towards ink so revealed words keep their contrast.\n        \"[--grt-a:color-mix(in_oklab,var(--grt-accent)_82%,#18181b)] [--grt-b:color-mix(in_oklab,var(--grt-accent-2)_48%,#18181b)] \" +\n        \"dark:[--grt-a:var(--grt-accent)] dark:[--grt-b:var(--grt-accent-2)] \" +\n        className\n      }\n      style={{\n        [\"--grt-accent\" as string]: accent,\n        [\"--grt-accent-2\" as string]: accentSecondary,\n        [\"--grt-dim\" as string]: Math.min(1, Math.max(0, dimOpacity)),\n        ...style,\n      }}\n      {...props}\n    >\n      <span className=\"sr-only\">{children}</span>\n      <span aria-hidden=\"true\">\n        {words.map((word, i) => {\n          const color = wordColor(i, total);\n          return (\n            <React.Fragment key={i}>\n              {i > 0 && \" \"}\n              {reduceMotion ? (\n                // Static, fully revealed version.\n                <span style={{ color }}>{word}</span>\n              ) : (\n                <RevealWord\n                  word={word}\n                  color={color}\n                  progress={progress}\n                  order={direction === \"forward\" ? i : total - 1 - i}\n                  total={total}\n                  softness={Math.max(1, softness)}\n                />\n              )}\n            </React.Fragment>\n          );\n        })}\n      </span>\n    </Tag>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "text"
  ],
  "docs": "Documentation: https://wancraft.my/components/gradient-reveal-text"
}
