UNPKG

reactbits-mcp-server

Version:

MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements

182 lines (162 loc) 4.77 kB
"use client"; import { useEffect, useRef, useState, createElement } 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 = Array.isArray(text) ? text : [text]; const getRandomSpeed = () => { if (!variableSpeed) return typingSpeed; const { min, max } = variableSpeed; return Math.random() * (max - min) + min; }; 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, }, <span className="text-type__content" style={{ color: getCurrentTextColor() }} > {displayedText} </span>, showCursor && ( <span ref={cursorRef} className={`text-type__cursor ${cursorClassName} ${shouldHideCursor ? "text-type__cursor--hidden" : ""}`} > {cursorCharacter} </span> ) ); }; export default TextType;