{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-reveal",
  "type": "registry:ui",
  "title": "Scroll Reveal",
  "description": "The ScrollReveal component lights up text word by word as the reader scrolls past it, tied to scroll position rather than a timer.",
  "author": "Claude Code (https://claude.com/claude-code)",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/demo/text/scroll-reveal.tsx",
      "content": "\"use client\";\n\nimport {\n  motion,\n  useScroll,\n  useTransform,\n  useReducedMotion,\n  type MotionValue,\n} from \"motion/react\";\nimport { useRef } from \"react\";\n\ninterface ScrollRevealProps {\n  children: string;\n  /** Opacity of a word before it is reached. */\n  restingOpacity?: number;\n  /** How much of the viewport the reveal is spread across, 0-1. */\n  spread?: number;\n  className?: string;\n}\n\n/**\n * Text that lights up word by word as the reader scrolls past it.\n *\n * Each word maps its own slice of the container's scroll progress, so the\n * reveal tracks the scrollbar exactly — scroll back up and it un-reveals.\n * A time-based stagger would drift out of sync with the reader.\n */\nexport function ScrollReveal({\n  children,\n  restingOpacity = 0.15,\n  spread = 0.5,\n  className = \"\",\n}: ScrollRevealProps) {\n  const ref = useRef<HTMLParagraphElement>(null);\n  const prefersReducedMotion = useReducedMotion();\n\n  const { scrollYProgress } = useScroll({\n    target: ref,\n    offset: [\"start 0.9\", `end ${spread}`],\n  });\n\n  const words = children.split(\" \");\n\n  if (prefersReducedMotion) {\n    return <p className={className}>{children}</p>;\n  }\n\n  return (\n    <p ref={ref} className={`flex flex-wrap ${className}`}>\n      {words.map((word, index) => {\n        const start = index / words.length;\n        const end = start + 1 / words.length;\n\n        return (\n          <Word\n            key={`${word}-${index}`}\n            progress={scrollYProgress}\n            range={[start, end]}\n            restingOpacity={restingOpacity}\n          >\n            {word}\n          </Word>\n        );\n      })}\n    </p>\n  );\n}\n\ninterface WordProps {\n  children: string;\n  progress: MotionValue<number>;\n  range: [number, number];\n  restingOpacity: number;\n}\n\nfunction Word({ children, progress, range, restingOpacity }: WordProps) {\n  const opacity = useTransform(progress, range, [restingOpacity, 1]);\n  // A touch of blur on the way in makes the arrival read as focus rather than\n  // as a flat fade.\n  const filter = useTransform(progress, range, [\n    \"blur(4px)\",\n    \"blur(0px)\",\n  ] as const);\n\n  return (\n    <span className=\"relative mr-[0.25em] inline-block\">\n      <motion.span style={{ opacity, filter }}>{children}</motion.span>\n    </span>\n  );\n}\n\nexport default ScrollReveal;\n",
      "type": "registry:ui",
      "target": "components/ui/scroll-reveal.tsx"
    }
  ]
}
