{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-reveal",
  "title": "Text Reveal",
  "description": "Word-by-word text reveal — tokens rise and fade in sequence as the line enters view.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://motion.asmitsah.dev/r/provider.json"
  ],
  "files": [
    {
      "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"
    }
  ],
  "type": "registry:component"
}