'use client'; import { useEffect, useRef, useState, createElement, useMemo, useCallback } from 'react'; import { gsap } from 'gsap'; import './TextType.css'; const TextType = ({ text, as: Component = 'div', typingSpeed = 50, initialDelay = 0, pauseDuration = 2000, deletingSpeed = 30, loop = true, className = '', showCursor = true, hideCursorWhileTyping = false, cursorCharacter = '|', cursorClassName = '', cursorBlinkDuration = 0.5, textColors = [], variableSpeed, onSentenceComplete, startOnVisible = false, reverseMode = false, ...props }) => { const [displayedText, setDisplayedText] = useState(''); const [currentCharIndex, setCurrentCharIndex] = useState(0); const [isDeleting, setIsDeleting] = useState(false); const [currentTextIndex, setCurrentTextIndex] = useState(0); const [isVisible, setIsVisible] = useState(!startOnVisible); const cursorRef = useRef(null); const containerRef = useRef(null); const textArray = useMemo(() => (Array.isArray(text) ? text : [text]), [text]); const getRandomSpeed = useCallback(() => { if (!variableSpeed) return typingSpeed; const { min, max } = variableSpeed; return Math.random() * (max - min) + min; }, [variableSpeed, typingSpeed]); const getCurrentTextColor = () => { if (textColors.length === 0) return '#ffffff'; return textColors[currentTextIndex % textColors.length]; }; useEffect(() => { if (!startOnVisible || !containerRef.current) return; const observer = new IntersectionObserver( entries => { entries.forEach(entry => { if (entry.isIntersecting) { setIsVisible(true); } }); }, { threshold: 0.1 } ); observer.observe(containerRef.current); return () => observer.disconnect(); }, [startOnVisible]); useEffect(() => { if (showCursor && cursorRef.current) { gsap.set(cursorRef.current, { opacity: 1 }); gsap.to(cursorRef.current, { opacity: 0, duration: cursorBlinkDuration, repeat: -1, yoyo: true, ease: 'power2.inOut' }); } }, [showCursor, cursorBlinkDuration]); useEffect(() => { if (!isVisible) return; let timeout; const currentText = textArray[currentTextIndex]; const processedText = reverseMode ? currentText.split('').reverse().join('') : currentText; const executeTypingAnimation = () => { if (isDeleting) { if (displayedText === '') { setIsDeleting(false); if (currentTextIndex === textArray.length - 1 && !loop) { return; } if (onSentenceComplete) { onSentenceComplete(textArray[currentTextIndex], currentTextIndex); } setCurrentTextIndex(prev => (prev + 1) % textArray.length); setCurrentCharIndex(0); timeout = setTimeout(() => {}, pauseDuration); } else { timeout = setTimeout(() => { setDisplayedText(prev => prev.slice(0, -1)); }, deletingSpeed); } } else { if (currentCharIndex < processedText.length) { timeout = setTimeout( () => { setDisplayedText(prev => prev + processedText[currentCharIndex]); setCurrentCharIndex(prev => prev + 1); }, variableSpeed ? getRandomSpeed() : typingSpeed ); } else if (textArray.length > 1) { timeout = setTimeout(() => { setIsDeleting(true); }, pauseDuration); } } }; if (currentCharIndex === 0 && !isDeleting && displayedText === '') { timeout = setTimeout(executeTypingAnimation, initialDelay); } else { executeTypingAnimation(); } return () => clearTimeout(timeout); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ currentCharIndex, displayedText, isDeleting, typingSpeed, deletingSpeed, pauseDuration, textArray, currentTextIndex, loop, initialDelay, isVisible, reverseMode, variableSpeed, onSentenceComplete ]); const shouldHideCursor = hideCursorWhileTyping && (currentCharIndex < textArray[currentTextIndex].length || isDeleting); return createElement( Component, { ref: containerRef, className: `text-type ${className}`, ...props }, {displayedText} , showCursor && ( {cursorCharacter} ) ); }; export default TextType;