{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-reveal",
  "type": "registry:ui",
  "title": "Text reveal",
  "description": "Reveal a short piece of content with restrained motion.",
  "categories": [
    "text"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/text-reveal/text-reveal.tsx",
      "content": "\"use client\";\nimport { Fragment, type CSSProperties } from \"react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\n/**\n * Reveals a short headline or sentence once, as it mounts: each word rises out of its own clip while it sharpens from a soft blur.\n * The entrance runs in CSS, so it starts on first paint, never waits for hydration, and never leaves text hidden when scripts are slow.\n * Use `\\n` in `text` for a deliberate line break and change the element's `key` to replay it.\n */\nexport interface TextRevealProps {\n  text: string;\n  as?: \"h1\" | \"h2\" | \"h3\" | \"p\";\n  className?: string;\n  id?: string;\n  /** Seconds to wait before the first word rises. Defaults to 0. */ delay?: number;\n}\n/** Total stagger stays under this many seconds, however long the text is. */\nconst MAX_STAGGER = motionTokens.duration.considered;\n// Balance is important: the base stylesheet sets `p { text-wrap: pretty }` outside any layer, which would otherwise beat the utility.\n/** The clip hides the rise below the line through a feathered bottom edge, so a blurred word surfaces softly instead of being cut by a hard line.\n *  Padding with matching negative margins leaves room for descenders and the blur without changing layout; the feather sits below the descenders, so resting words are never dimmed. */\nconst clip =\n  \"inline-block align-top -mx-[.16em] -mt-[.1em] -mb-[.24em] px-[.16em] pt-[.1em] pb-[.24em] [mask-image:linear-gradient(#000_calc(100%_-_.18em),transparent)] motion-reduce:[mask-image:none]\";\n\nexport function TextReveal({ text, as = \"h2\", className, id, delay = 0 }: TextRevealProps) {\n  const Tag = as;\n  const lines = text.split(\"\\n\").map((line) => line.split(\" \").filter(Boolean));\n  const count = lines.reduce((total, words) => total + words.length, 0);\n  const step = Math.min(motionTokens.stagger.word, MAX_STAGGER / Math.max(count, 1));\n  const blur = as === \"p\" ? motionTokens.blur.soft : motionTokens.blur.text;\n  let index = 0;\n  const spoken = text.replace(/\\n/g, \" \");\n  // Headings take their accessible name from aria-label, so the sentence appears once in the DOM. A paragraph cannot be named, so it keeps a visually hidden copy.\n  const named = as !== \"p\";\n  return (\n    <Tag\n      id={id}\n      aria-label={named ? spoken : undefined}\n      className={cn(\"m-0 max-w-[24ch] overflow-visible text-balance! text-ink\", className)}\n      style={{ \"--reveal-blur\": `${blur}px` } as CSSProperties}\n    >\n      {named ? null : <span className=\"sr-only select-none\">{spoken}</span>}\n      {lines.map((words, lineIndex) => (\n        <Fragment key={lineIndex}>\n          <span className=\"block\" aria-hidden=\"true\">\n            {words.map((word, wordIndex) => {\n              const position = index++;\n              return (\n                <Fragment key={`${word}-${position}`}>\n                  <span className={clip}>\n                    <span\n                      className=\"inline-block animate-text-reveal-word motion-reduce:animate-text-reveal-still\"\n                      style={{ \"--reveal-delay\": `${delay + position * step + lineIndex * step}s` } as CSSProperties}\n                    >\n                      {word}\n                    </span>\n                  </span>\n                  {wordIndex < words.length - 1 ? \" \" : null}\n                </Fragment>\n              );\n            })}\n          </span>\n          {lineIndex < lines.length - 1 ? \" \" : null}\n        </Fragment>\n      ))}\n    </Tag>\n  );\n}\n\nexport default TextReveal;\n",
      "type": "registry:component",
      "target": "@components/iris/text-reveal/text-reveal.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/text-reveal",
  "meta": {
    "kind": "component",
    "group": "Text & motion",
    "export": "TextReveal",
    "tags": [
      "text",
      "motion",
      "content"
    ],
    "docs": "https://irisui.dev/components/text-reveal"
  }
}
