{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"text-reveal","type":"registry:ui","title":"Text Reveal","description":"Words arrive in reading order.","dependencies":["motion"],"categories":["content"],"docs":"https://www.interior.dev/docs/text-reveal","files":[{"path":"registry/interior/text-reveal.tsx","type":"registry:ui","target":"components/interior/text-reveal.tsx","content":"\"use client\";\n\nimport { Fragment, useMemo, useRef } from \"react\";\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\n\nconst EASE = [0.23, 1, 0.32, 1] as const;\nconst DURATION = 0.6;\n\nconst HIDDEN = { opacity: 0, y: 10, filter: \"blur(8px)\" } as const;\nconst SHOWN = { opacity: 1, y: 0, filter: \"blur(0px)\" } as const;\n\nexport type TextRevealSplit = \"word\" | \"character\";\n\nexport type TextRevealUnit = {\n  key: string;\n  text: string;\n  index: number;\n};\n\nexport type TextRevealGroup = {\n  key: string;\n  units: TextRevealUnit[];\n};\n\nexport type UseTextRevealOptions = {\n  text: string;\n  by?: TextRevealSplit;\n  stagger?: number;\n  maxDuration?: number;\n  startOnView?: boolean;\n  play?: boolean;\n  once?: boolean;\n  amount?: number;\n};\n\nexport function useTextReveal<T extends HTMLElement = HTMLSpanElement>({\n  text,\n  by = \"word\",\n  stagger = 0.055,\n  maxDuration = 1.6,\n  startOnView = true,\n  play = true,\n  once = true,\n  amount = 0.35,\n}: UseTextRevealOptions) {\n  const ref = useRef<T>(null);\n  const inView = useInView(ref, { once, amount });\n  const reduced = useReducedMotion();\n\n  const { groups, step, count } = useMemo(() => {\n    const words = text.trim().length ? text.trim().split(/\\s+/) : [];\n\n    let index = 0;\n    const built: TextRevealGroup[] = words.map((word, w) => {\n      if (by === \"character\") {\n        return {\n          key: `w${w}`,\n          units: Array.from(word).map((char, c) => ({\n            key: `w${w}c${c}`,\n            text: char,\n            index: index++,\n          })),\n        };\n      }\n      return {\n        key: `w${w}`,\n        units: [{ key: `w${w}`, text: word, index: index++ }],\n      };\n    });\n\n    const total = index;\n    const span = Math.max(0, maxDuration - DURATION);\n\n    return {\n      groups: built,\n      count: total,\n      step: total > 1 ? Math.min(stagger, span / (total - 1)) : 0,\n    };\n  }, [text, by, stagger, maxDuration]);\n\n  const started = play && (!startOnView || inView);\n\n  return {\n    ref,\n    groups,\n    step,\n    count,\n    started,\n    reduced: Boolean(reduced),\n    duration: count > 1 ? (count - 1) * step + DURATION : DURATION,\n  };\n}\n\nexport type TextRevealProps = UseTextRevealOptions & {\n  className?: string;\n};\n\nexport function TextReveal({\n  text,\n  by = \"word\",\n  stagger = 0.055,\n  maxDuration = 1.6,\n  startOnView = true,\n  play = true,\n  once = true,\n  amount = 0.35,\n  className = \"\",\n}: TextRevealProps) {\n  const { ref, groups, step, started, reduced } = useTextReveal<HTMLSpanElement>(\n    { text, by, stagger, maxDuration, startOnView, play, once, amount },\n  );\n\n  return (\n    <span ref={ref} className={`text-stone-700 dark:text-stone-200 ${className}`}>\n      <span className=\"sr-only\">{text}</span>\n\n      <span aria-hidden=\"true\">\n        {groups.map((group, g) => (\n          <Fragment key={group.key}>\n            {g > 0 ? \" \" : null}\n            <span className=\"inline-block whitespace-nowrap align-baseline\">\n              {group.units.map((unit) => (\n                <motion.span\n                  key={unit.key}\n                  className=\"inline-block align-baseline\"\n                  initial={reduced ? false : HIDDEN}\n                  animate={started ? SHOWN : HIDDEN}\n                  transition={\n                    reduced\n                      ? { duration: 0 }\n                      : {\n                          duration: DURATION,\n                          ease: EASE,\n                          delay: started ? unit.index * step : 0,\n                        }\n                  }\n                >\n                  {unit.text}\n                </motion.span>\n              ))}\n            </span>\n          </Fragment>\n        ))}\n      </span>\n    </span>\n  );\n}\n"}]}