{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mask-text-reveal",
  "title": "Mask Text Reveal",
  "description": "Masked text reveal — words rise out from behind a clip mask, token by token.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://motion.asmitsah.dev/r/provider.json"
  ],
  "files": [
    {
      "path": "components/motion/mask-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, maskTextItem, textRevealContainer, 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 MaskTextRevealProps = {\n  /** A single host element (h1–h6 / p / span / div) whose own children is a\n   *  plain string. MaskTextReveal 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\" | \"line\";\n  trigger?: \"inView\" | \"mount\";\n};\n\n/** Text slides up out of a hard mask — no fade, crisp editorial. Per token an\n *  overflow-hidden wrapper clips an inner span that rises from below (110% → 0). */\nexport function MaskTextReveal({\n  children,\n  delay = 0,\n  by = \"word\",\n  trigger = \"inView\",\n}: MaskTextRevealProps) {\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 step = by === \"line\" ? feel.lineStagger : feel.textStagger;\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 masks stay aria-hidden.\n  const isHeading = tag.length === 2 && tag.startsWith(\"h\");\n  const tokens =\n    by === \"line\"\n      ? text\n          .split(\"\\n\")\n          .map((line) => line.trim())\n          .filter(Boolean)\n      : text.trim().split(/\\s+/);\n\n  return (\n    // aria-label carries the full string so SR reads it whole, not per-mask.\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      {tokens.map((token, ti) => {\n        const tokenKey = `t${ti}`;\n        return (\n          <Fragment key={tokenKey}>\n            <span\n              aria-hidden\n              style={{ display: by === \"line\" ? \"block\" : \"inline-block\", overflow: \"hidden\" }}\n            >\n              <m.span variants={maskTextItem} style={{ display: \"inline-block\" }}>\n                {token}\n              </m.span>\n            </span>\n            {by === \"word\" && ti < tokens.length - 1 ? \" \" : null}\n          </Fragment>\n        );\n      })}\n    </Container>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/mask-text-reveal.tsx"
    }
  ],
  "type": "registry:component"
}