{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-reveal",
  "title": "Text Reveal",
  "description": "Dims and brightens words as the text scrolls through the viewport.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/text-reveal/text-reveal.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/** Props for {@link TextReveal}. */\nexport type TextRevealProps = React.ComponentPropsWithoutRef<\"div\"> & {\n  /** Text whose words brighten as the block scrolls through the viewport. */\n  children: string;\n};\n\nfunction usePrefersReducedMotion(): boolean {\n  const [reduced, setReduced] = React.useState(false);\n\n  React.useEffect(() => {\n    if (\n      typeof window === \"undefined\" ||\n      typeof window.matchMedia !== \"function\"\n    ) {\n      return;\n    }\n\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const onChange = (): void => {\n      setReduced(query.matches);\n    };\n\n    onChange();\n    query.addEventListener(\"change\", onChange);\n\n    return () => {\n      query.removeEventListener(\"change\", onChange);\n    };\n  }, []);\n\n  return reduced;\n}\n\nfunction clamp(value: number): number {\n  return Math.min(Math.max(value, 0), 1);\n}\n\nfunction useScrollProgress(\n  nodeRef: React.RefObject<HTMLDivElement | null>,\n  enabled: boolean,\n): number {\n  const [progress, setProgress] = React.useState(enabled ? 0 : 1);\n\n  React.useEffect(() => {\n    const node = nodeRef.current;\n    if (!enabled || !node || typeof window === \"undefined\") {\n      setProgress(1);\n      return;\n    }\n    const onScroll = (): void => {\n      const bounds = node.getBoundingClientRect();\n      const span = bounds.height + window.innerHeight;\n      setProgress(clamp((window.innerHeight - bounds.top) / span));\n    };\n    onScroll();\n    window.addEventListener(\"scroll\", onScroll, { passive: true });\n    return () => {\n      window.removeEventListener(\"scroll\", onScroll);\n    };\n  }, [enabled, nodeRef]);\n\n  return progress;\n}\n\nfunction wordOpacity(progress: number, total: number, index: number): number {\n  return Math.min(Math.max(progress * total - index, 0.2), 1);\n}\n\n/**\n * Brightens each word in turn as the block scrolls through the viewport.\n *\n * Respects `prefers-reduced-motion`: every word stays full opacity.\n *\n * @example\n * ```tsx\n * <TextReveal>Scroll to read this line word by word</TextReveal>\n * ```\n */\nexport const TextReveal = ({\n  children,\n  className,\n  ref,\n  ...props\n}: TextRevealProps & { ref?: React.Ref<HTMLDivElement> }) => {\n  const reduced = usePrefersReducedMotion();\n  const nodeRef = React.useRef<HTMLDivElement>(null);\n  const progress = useScrollProgress(nodeRef, !reduced);\n  const words = children.split(\" \");\n\n  return (\n    <div\n      aria-label={children}\n      className={cn(\"flex flex-wrap gap-x-[0.25em]\", className)}\n      ref={(node) => {\n        nodeRef.current = node;\n        if (typeof ref === \"function\") {\n          ref(node);\n        } else if (ref) {\n          ref.current = node;\n        }\n      }}\n      {...props}\n    >\n      {words.map((word, index) => (\n        <span\n          aria-hidden=\"true\"\n          className=\"text-foreground transition-opacity duration-300\"\n          key={`${word}-${index}`}\n          style={{ opacity: wordOpacity(progress, words.length, index) }}\n        >\n          {word}\n        </span>\n      ))}\n    </div>\n  );\n};\nTextReveal.displayName = \"TextReveal\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
