(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 containerClasses = [
styles.container,
styles[speed],
className
].filter(Boolean).join(' ');
const renderWord = (word: string, index: number) => {
const isVisible = visibleIndices.has(index);
const wordClasses = [
styles.word,
styles[direction],
effect !== 'none' && styles[effect],
isVisible && styles.visible,
highlight && styles.highlight
].filter(Boolean).join(' ');
return (
{word}
);
};
const renderChar = (char: string, index: number) => {
const isVisible = visibleIndices.has(index);
return (
{char === ' ' ? '\u00A0' : char}
);
};
const renderLine = (line: string, index: number) => {
const isVisible = visibleIndices.has(index);
const lineClasses = [
styles.line,
styles[direction],
isVisible && styles.visible
].filter(Boolean).join(' ');
return (
{line}
);
};
return (
{
(containerRef as React.MutableRefObject).current = node;
if (typeof ref === 'function') ref(node);
else if (ref) ref.current = node;
}}
className={containerClasses}
style={style}
{...props}
>
{elements.map((el, i) =>
splitBy === 'line'
? renderLine(el, i)
: splitBy === 'char'
? renderChar(el, i)
: renderWord(el, i)
)}
);
}
);
RevealText.displayName = 'RevealText';
export default RevealText;