import { useRef, useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText as GSAPSplitText } from "gsap/SplitText";
gsap.registerPlugin(ScrollTrigger, GSAPSplitText);
const SplitText = ({
text,
className = "",
delay = 100,
duration = 0.6,
ease = "power3.out",
splitType = "chars",
from = { opacity: 0, y: 40 },
to = { opacity: 1, y: 0 },
threshold = 0.1,
rootMargin = "-100px",
textAlign = "center",
onLetterAnimationComplete,
}) => {
const ref = useRef(null);
const animationCompletedRef = useRef(false);
const scrollTriggerRef = useRef(null);
useEffect(() => {
if (typeof window === "undefined" || !ref.current || !text) return;
const el = ref.current;
animationCompletedRef.current = false;
const absoluteLines = splitType === "lines";
if (absoluteLines) el.style.position = "relative";
let splitter;
try {
splitter = new GSAPSplitText(el, {
type: splitType,
absolute: absoluteLines,
linesClass: "split-line",
});
} catch (error) {
console.error("Failed to create SplitText:", error);
return;
}
let targets;
switch (splitType) {
case "lines":
targets = splitter.lines;
break;
case "words":
targets = splitter.words;
break;
case "chars":
targets = splitter.chars;
break;
default:
targets = splitter.chars;
}
if (!targets || targets.length === 0) {
console.warn("No targets found for SplitText animation");
splitter.revert();
return;
}
targets.forEach((t) => {
t.style.willChange = "transform, opacity";
});
const startPct = (1 - threshold) * 100;
const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(rootMargin);
const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0;
const marginUnit = marginMatch ? (marginMatch[2] || "px") : "px";
const sign = marginValue < 0 ? `-=${Math.abs(marginValue)}${marginUnit}` : `+=${marginValue}${marginUnit}`;
const start = `top ${startPct}%${sign}`;
const tl = gsap.timeline({
scrollTrigger: {
trigger: el,
start,
toggleActions: "play none none none",
once: true,
onToggle: (self) => {
scrollTriggerRef.current = self;
},
},
smoothChildTiming: true,
onComplete: () => {
animationCompletedRef.current = true;
gsap.set(targets, {
...to,
clearProps: "willChange",
immediateRender: true,
});
onLetterAnimationComplete?.();
},
});
tl.set(targets, { ...from, immediateRender: false, force3D: true });
tl.to(targets, {
...to,
duration,
ease,
stagger: delay / 1000,
force3D: true,
});
return () => {
tl.kill();
if (scrollTriggerRef.current) {
scrollTriggerRef.current.kill();
scrollTriggerRef.current = null;
}
gsap.killTweensOf(targets);
if (splitter) {
splitter.revert();
}
};
}, [
text,
delay,
duration,
ease,
splitType,
from,
to,
threshold,
rootMargin,
onLetterAnimationComplete,
]);
return (
{text}
);
};
export default SplitText;