{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-text",
  "type": "registry:ui",
  "title": "Split Text",
  "description": "Splits text into characters, words, or lines and reveals them with a staggered, optionally scroll-triggered animation.",
  "files": [
    {
      "path": "registry/sonaui/split-text/split-text.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { ScrollTrigger } from \"gsap/ScrollTrigger\";\nimport { SplitText as GSAPSplitText } from \"gsap/SplitText\";\nimport { type ReactNode, useRef } from \"react\";\n\nimport { cn } from \"@/lib/sona-utils\";\n\ngsap.registerPlugin(useGSAP, GSAPSplitText, ScrollTrigger);\n\ntype SplitUnit = \"chars\" | \"words\" | \"lines\";\n\nexport interface SplitTextProps {\n  /** A single text element (e.g. a heading or paragraph) to split and animate. */\n  children: ReactNode;\n  /** Additional CSS classes for the wrapper. */\n  className?: string;\n  /**\n   * Which unit the text is split into and animated.\n   * @default \"words\"\n   */\n  variant?: SplitUnit;\n  /**\n   * Mask each split piece (clip it) so it reveals from behind an edge instead\n   * of animating in fully visible. Masks by the same unit set in `variant`.\n   * @default true\n   */\n  mask?: boolean;\n  /**\n   * GSAP tween vars merged over the defaults, e.g. `{ duration: 1.2 }`.\n   * @default { yPercent: 120, rotate: 5, stagger: 0.2, duration: 0.4 }\n   */\n  animationProps?: gsap.TweenVars;\n  /**\n   * Play the animation when the element scrolls into view instead of on mount.\n   * @default false\n   */\n  scrollTrigger?: boolean;\n  /**\n   * ScrollTrigger start position (only used when `scrollTrigger` is true).\n   * @default \"top 85%\"\n   */\n  start?: string;\n  /**\n   * Re-split on resize so line breaks stay correct. Note: this replays the\n   * animation on every resize, so it suits looping reveals more than one-shot\n   * entrances. Font-load correctness is handled automatically either way.\n   * @default false\n   */\n  autoSplit?: boolean;\n  /**\n   * Show ScrollTrigger debug markers.\n   * @default false\n   */\n  markers?: boolean;\n}\n\nexport default function SplitText({\n  children,\n  className,\n  variant = \"words\",\n  mask = true,\n  animationProps = {},\n  scrollTrigger = false,\n  start = \"top 85%\",\n  autoSplit = false,\n  markers = false,\n}: SplitTextProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const defaultAnimationProps: gsap.TweenVars = {\n    yPercent: 120,\n    rotate: 5,\n    stagger: 0.2,\n    duration: 0.4,\n  };\n\n  const mergedAnimationProps = { ...defaultAnimationProps, ...animationProps };\n\n  useGSAP(\n    () => {\n      const containerEl = containerRef.current;\n      if (!containerEl) return;\n\n      // Reveal the wrapper (it starts hidden to avoid a flash of unsplit text).\n      gsap.set(containerEl, { opacity: 1 });\n\n      // Reduced motion: show the final text, skip the animation entirely.\n      const prefersReducedMotion = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\",\n      ).matches;\n      if (prefersReducedMotion) return;\n\n      const targetEl = containerEl.firstElementChild as HTMLElement | null;\n      if (!targetEl) {\n        console.warn(\n          \"[SplitText] Expected a single wrapping element as children (e.g. an <h2>), but found none. Nothing will animate.\",\n        );\n      }\n      if (!targetEl) return;\n\n      let split: GSAPSplitText | undefined;\n\n      // Build the split + animation. Kept in a closure so it can run after fonts\n      // load, and re-run per re-split when `autoSplit` is on.\n      const build = () => {\n        split = GSAPSplitText.create(targetEl, {\n          type: variant,\n          mask: mask ? variant : undefined,\n          autoSplit,\n          // Create the tween inside onSplit and return it so GSAP re-runs it on\n          // every re-split (font swap / resize when autoSplit is enabled).\n          onSplit: (self) => {\n            const tl = gsap.timeline(\n              scrollTrigger\n                ? { scrollTrigger: { trigger: containerEl, start, markers } }\n                : {},\n            );\n            tl.from(self[variant], mergedAnimationProps);\n            return tl;\n          },\n        });\n      };\n\n      // Wait for web fonts before splitting so `lines` and masks measure against\n      // the real font metrics, not the fallback. Resolves immediately if loaded.\n      let cancelled = false;\n      const fonts = typeof document !== \"undefined\" ? document.fonts : null;\n      if (fonts && fonts.status !== \"loaded\") {\n        fonts.ready.then(() => {\n          if (!cancelled) build();\n        });\n      } else {\n        build();\n      }\n\n      return () => {\n        cancelled = true;\n        split?.revert();\n      };\n    },\n    {\n      dependencies: [{ ...mergedAnimationProps }, variant, mask, scrollTrigger],\n      scope: containerRef,\n      revertOnUpdate: true,\n    },\n  );\n\n  return (\n    <div ref={containerRef} className={cn(\"opacity-0\", className)}>\n      {children}\n    </div>\n  );\n}\n",
      "target": "components/ui/split-text/split-text.tsx"
    }
  ],
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [
    "@sona-ui/sona-utils"
  ]
}