{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "line-reveal",
  "title": "Line Reveal",
  "description": "Per-line rise + fade, staggered and calm — for body copy / multiline blocks; each line clips inside its own mask.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://motion.asmitsah.dev/r/provider.json"
  ],
  "files": [
    {
      "path": "components/motion/line-reveal.tsx",
      "content": "\"use client\";\n\nimport * as m from \"motion/react-m\";\nimport { type CSSProperties, isValidElement, type ReactElement, type ReactNode } from \"react\";\nimport { lineRevealContainer, lineRevealItem, 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 LineRevealProps = {\n  /** A single host element (h1–h6 / p / span / div) whose own children is a\n   *  plain string (split on \"\\n\"). LineReveal reconstructs THAT element as the\n   *  animated one — its tag, className + style win; no extra DOM wrapper. */\n  children: ReactNode;\n  delay?: number;\n  trigger?: \"inView\" | \"mount\";\n};\n\n/** Per-line rise + fade, staggered and calm — for body copy / multiline blocks.\n *  Splits the wrapped element's string on \"\\n\"; each line clips inside its own\n *  overflow-hidden wrapper so the rise reads as an emerge, not a slide.\n *  Transform-only (provider handles reduced motion). aria-label carries the whole\n *  text so SR reads it as one passage. */\nexport function LineReveal({ children, delay = 0, trigger = \"inView\" }: LineRevealProps) {\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.div;\n  const { className, style } = child.props;\n  const lines = text\n    .split(\"\\n\")\n    .map((line) => line.trim())\n    .filter((line) => line.length > 0);\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-line masks stay aria-hidden.\n  const isHeading = tag.length === 2 && tag.startsWith(\"h\");\n\n  return (\n    <Container\n      className={className}\n      style={style}\n      aria-label={text}\n      role={isHeading ? undefined : \"img\"}\n      variants={lineRevealContainer(delay)}\n      initial=\"initial\"\n      {...play}\n    >\n      {lines.map((line, li) => {\n        const lineKey = `l${li}`;\n        return (\n          <span key={lineKey} aria-hidden style={{ display: \"block\", overflow: \"hidden\" }}>\n            <m.span variants={lineRevealItem} style={{ display: \"block\" }}>\n              {line}\n            </m.span>\n          </span>\n        );\n      })}\n    </Container>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/line-reveal.tsx"
    }
  ],
  "type": "registry:component"
}