{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-text",
  "title": "Split Text",
  "description": "A heading animated one character, word, or line at a time, announced as one sentence.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/split-text/split-text.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type SplitBy = \"character\" | \"word\" | \"line\"\n\nexport type SplitAnimation = \"rise\" | \"fade\" | \"blur\" | \"scale\"\n\nexport type SplitTextProps = Omit<\n  React.HTMLAttributes<HTMLSpanElement>,\n  \"children\"\n> & {\n  children: string\n  by?: SplitBy\n  animation?: SplitAnimation\n  /** Milliseconds between one piece and the next. */\n  stagger?: number\n  delay?: number\n  duration?: number\n  /** Whether it plays on arrival or as soon as it is on screen. */\n  trigger?: \"mount\" | \"view\"\n  as?: \"span\" | \"h1\" | \"h2\" | \"h3\" | \"p\"\n}\n\nconst RESTING: Record<SplitAnimation, string> = {\n  rise: \"translate-y-[0.6em] opacity-0\",\n  fade: \"opacity-0\",\n  blur: \"opacity-0 blur-[6px]\",\n  scale: \"scale-75 opacity-0\",\n}\n\nfunction pieces(text: string, by: SplitBy) {\n  if (by === \"line\") return text.split(\"\\n\")\n  if (by === \"word\") return text.split(/(\\s+)/)\n  return Array.from(text)\n}\n\ntype Piece = { part: string; index: number }\n\n/**\n * A line may only break between words. Every piece is its own inline-block,\n * which the browser is otherwise free to break between, so splitting by\n * character would let it cut \"deserve\" into \"des\" and \"erve\".\n */\nfunction runs(parts: string[]): Piece[][] {\n  const out: Piece[][] = []\n  let word: Piece[] = []\n\n  parts.forEach((part, index) => {\n    if (part.trim() === \"\") {\n      if (word.length) out.push(word)\n      out.push([{ part, index }])\n      word = []\n      return\n    }\n\n    word.push({ part, index })\n  })\n\n  if (word.length) out.push(word)\n  return out\n}\n\n/**\n * Animates a string one character, word, or line at a time. The whole string\n * is carried on the element as its label, so it is announced once as ordinary\n * text rather than as a pile of single letters.\n */\nexport function SplitText({\n  children,\n  by = \"character\",\n  animation = \"rise\",\n  stagger = 28,\n  delay = 0,\n  duration = 620,\n  trigger = \"view\",\n  as: Component = \"span\",\n  className,\n  ...rootProps\n}: SplitTextProps) {\n  const ref = React.useRef<HTMLSpanElement>(null)\n  const [shown, setShown] = React.useState(false)\n\n  React.useEffect(() => {\n    if (trigger !== \"mount\") return\n\n    // A frame first, so the resting state is painted and the transition has\n    // somewhere to travel from. Flipping it in the same tick as the first\n    // render leaves the characters already arrived, and nothing animates.\n    const frame = requestAnimationFrame(() => setShown(true))\n    return () => cancelAnimationFrame(frame)\n  }, [trigger])\n\n  React.useEffect(() => {\n    if (trigger === \"mount\") return\n\n    const element = ref.current\n    if (!element) return\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry?.isIntersecting) {\n          setShown(true)\n          observer.disconnect()\n        }\n      },\n      { threshold: 0.2 }\n    )\n\n    observer.observe(element)\n    return () => observer.disconnect()\n  }, [trigger])\n\n  const parts = React.useMemo(() => pieces(children, by), [children, by])\n\n  return (\n    <Component\n      ref={ref as React.Ref<HTMLHeadingElement>}\n      data-slot=\"split-text\"\n      aria-label={children}\n      className={cn(\"inline-block\", className)}\n      {...rootProps}\n    >\n      {runs(parts).map((word, at) => (\n        <span\n          key={`word-${at}`}\n          aria-hidden=\"true\"\n          className=\"inline-block whitespace-nowrap\"\n        >\n          {word.map(({ part, index }) => {\n            const blank = part.trim() === \"\"\n\n            return (\n              <span\n                key={`${part}-${index}`}\n                className={cn(\n                  \"inline-block whitespace-pre transition-[opacity,translate,scale,filter] ease-out\",\n                  \"motion-reduce:translate-none motion-reduce:scale-100 motion-reduce:opacity-100 motion-reduce:blur-none motion-reduce:transition-none\",\n                  shown || blank\n                    ? \"translate-none scale-100 opacity-100 blur-none\"\n                    : RESTING[animation]\n                )}\n                style={{\n                  transitionDelay: `${delay + index * stagger}ms`,\n                  transitionDuration: `${duration}ms`,\n                }}\n              >\n                {by === \"line\" && index < parts.length - 1 ? `${part}\\n` : part}\n              </span>\n            )\n          })}\n        </span>\n      ))}\n    </Component>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "motion",
    "text",
    "typography",
    "entrance"
  ],
  "type": "registry:ui"
}