{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SplitText-JS-CSS",
	"title": "SplitText",
	"description": "Splits text into characters / words for staggered entrance animation.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "SplitText/SplitText.jsx",
			"content": "import { useRef, useEffect, useState } from 'react';\nimport { gsap } from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\nimport { SplitText as GSAPSplitText } from 'gsap/SplitText';\nimport { useGSAP } from '@gsap/react';\n\ngsap.registerPlugin(ScrollTrigger, GSAPSplitText, useGSAP);\n\nconst SplitText = ({\n  text,\n  className = '',\n  delay = 50,\n  duration = 1.25,\n  ease = 'power3.out',\n  splitType = 'chars',\n  from = { opacity: 0, y: 40 },\n  to = { opacity: 1, y: 0 },\n  threshold = 0.1,\n  rootMargin = '-100px',\n  textAlign = 'center',\n  tag = 'p',\n  onLetterAnimationComplete\n}) => {\n  const ref = useRef(null);\n  const animationCompletedRef = useRef(false);\n  const onCompleteRef = useRef(onLetterAnimationComplete);\n  const [fontsLoaded, setFontsLoaded] = useState(false);\n\n  // Keep callback ref updated\n  useEffect(() => {\n    onCompleteRef.current = onLetterAnimationComplete;\n  }, [onLetterAnimationComplete]);\n\n  useEffect(() => {\n    if (document.fonts.status === 'loaded') {\n      setFontsLoaded(true);\n    } else {\n      document.fonts.ready.then(() => {\n        setFontsLoaded(true);\n      });\n    }\n  }, []);\n\n  useGSAP(\n    () => {\n      if (!ref.current || !text || !fontsLoaded) return;\n      // Prevent re-animation if already completed\n      if (animationCompletedRef.current) return;\n      const el = ref.current;\n\n      if (el._rbsplitInstance) {\n        try {\n          el._rbsplitInstance.revert();\n        } catch (_) {\n          /* noop */\n        }\n        el._rbsplitInstance = null;\n      }\n\n      const startPct = (1 - threshold) * 100;\n      const marginMatch = /^(-?\\d+(?:\\.\\d+)?)(px|em|rem|%)?$/.exec(rootMargin);\n      const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0;\n      const marginUnit = marginMatch ? marginMatch[2] || 'px' : 'px';\n      const sign =\n        marginValue === 0\n          ? ''\n          : marginValue < 0\n            ? `-=${Math.abs(marginValue)}${marginUnit}`\n            : `+=${marginValue}${marginUnit}`;\n      const start = `top ${startPct}%${sign}`;\n\n      let targets;\n      const assignTargets = self => {\n        if (splitType.includes('chars') && self.chars.length) targets = self.chars;\n        if (!targets && splitType.includes('words') && self.words.length) targets = self.words;\n        if (!targets && splitType.includes('lines') && self.lines.length) targets = self.lines;\n        if (!targets) targets = self.chars || self.words || self.lines;\n      };\n\n      const splitInstance = new GSAPSplitText(el, {\n        type: splitType,\n        smartWrap: true,\n        autoSplit: splitType === 'lines',\n        linesClass: 'split-line',\n        wordsClass: 'split-word',\n        charsClass: 'split-char',\n        reduceWhiteSpace: false,\n        onSplit: self => {\n          assignTargets(self);\n          const tween = gsap.fromTo(\n            targets,\n            { ...from },\n            {\n              ...to,\n              duration,\n              ease,\n              stagger: delay / 1000,\n              scrollTrigger: {\n                trigger: el,\n                start,\n                once: true,\n                fastScrollEnd: true,\n                anticipatePin: 0.4\n              },\n              onComplete: () => {\n                animationCompletedRef.current = true;\n                onCompleteRef.current?.();\n              },\n              willChange: 'transform, opacity',\n              force3D: true\n            }\n          );\n          return tween;\n        }\n      });\n\n      el._rbsplitInstance = splitInstance;\n\n      return () => {\n        ScrollTrigger.getAll().forEach(st => {\n          if (st.trigger === el) st.kill();\n        });\n        try {\n          splitInstance.revert();\n        } catch (_) {\n          /* noop */\n        }\n        el._rbsplitInstance = null;\n      };\n    },\n    {\n      dependencies: [\n        text,\n        delay,\n        duration,\n        ease,\n        splitType,\n        JSON.stringify(from),\n        JSON.stringify(to),\n        threshold,\n        rootMargin,\n        fontsLoaded\n      ],\n      scope: ref\n    }\n  );\n\n  const renderTag = () => {\n    const style = {\n      textAlign,\n      overflow: 'hidden',\n      display: 'inline-block',\n      whiteSpace: 'normal',\n      wordWrap: 'break-word',\n      willChange: 'transform, opacity'\n    };\n    const classes = `split-parent ${className}`;\n    const Tag = tag || 'p';\n\n    return (\n      <Tag ref={ref} style={style} className={classes}>\n        {text}\n      </Tag>\n    );\n  };\n  return renderTag();\n};\n\nexport default SplitText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0",
		"@gsap/react@^2.1.2"
	]
}