(null);
const hasAnimated = useRef(false);
const elements = splitBy === 'line'
? children.split('\n')
: splitBy === 'char'
? children.split('')
: children.split(' ');
useEffect(() => {
if (immediate) {
elements.forEach((_, i) => {
setTimeout(() => {
setVisibleIndices(prev => new Set(prev).add(i));
}, i * stagger);
});
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && (!once || !hasAnimated.current)) {
hasAnimated.current = true;
elements.forEach((_, i) => {
setTimeout(() => {
setVisibleIndices(prev => new Set(prev).add(i));
}, i * stagger);
});
} else if (!entry.isIntersecting && !once) {
setVisibleIndices(new Set());
}
},
{ threshold }
);
if (containerRef.current) observer.observe(containerRef.current);
return () => observer.disconnect();
}, [elements, stagger, threshold, once, immediate]);
const renderElement = (text: string, index: number) => {
const isVisible = visibleIndices.has(index);
const duration = speedDurations[speed];
const baseClasses = `inline-block overflow-hidden ${splitBy === 'line' ? 'block mb-1' : 'mr-1'}`;
const innerBase = `inline-block transition-all ease-out ${duration}`;
const hiddenState = `${hiddenTransforms[direction]} opacity-0 ${effect === 'blur' ? 'blur-sm' : ''} ${effect === 'scale' ? 'scale-75' : ''}`;
const visibleState = 'translate-y-0 translate-x-0 opacity-100 blur-0 scale-100';
return (
{text === ' ' ? '\u00A0' : text}
{highlight && (
)}
);
};
return (
{
(containerRef as React.MutableRefObject).current = node;
if (typeof ref === 'function') ref(node);
else if (ref) ref.current = node;
}}
className={`relative ${className}`}
{...props}
>
{elements.map(renderElement)}
);
}
);
RevealText.displayName = 'RevealText';
export default RevealText;