Source

AnimatedText.js

/**
 * @fileoverview Animated Text Component for React applications
 * @module AnimatedText
 * @requires react
 * @version 1.0.0
 */

import React, { useState, useEffect } from 'react';

/**
 * @typedef {Object} AnimatedTextConfig
 * @property {number} [speed=100] - Animation speed in milliseconds
 * @property {boolean} [loop=false] - Whether to loop the animation
 * @property {number} [delay=0] - Delay before animation starts
 * @property {string} [color='currentColor'] - Text color
 */

/**
 * @typedef {Object} AnimatedTextProps
 * @property {string} text - The text to animate
 * @property {('typewriter'|'fadeIn'|'bounce'|'glitch'|'rainbow')} [effect='typewriter'] - Animation effect
 * @property {AnimatedTextConfig} [config] - Animation configuration
 * @property {Object} [styles] - Custom CSS styles
 */

class TextErrorBoundary extends React.Component {
    state = { hasError: false };

    static getDerivedStateFromError(error) {
        return { hasError: true };
    }

    render() {
        if (this.state.hasError) {
            return <span>Animation failed to load.</span>;
        }
        return this.props.children;
    }
}

/**
 * AnimatedText Component
 * @param {AnimatedTextProps} props - Component props
 * @returns {React.ReactElement} Rendered component
 */
const AnimatedText = ({
    text = '',
    effect = 'typewriter',
    config = {},
    styles = {}
}) => {
    const defaultConfig = {
        speed: 100,
        loop: false,
        delay: 0,
        color: 'currentColor',
        ...config
    };

    const defaultStyles = {
        base: {
            display: 'inline-block',
            fontFamily: 'inherit',
            color: defaultConfig.color,
        },
        typewriter: {
            whiteSpace: 'pre',
            overflow: 'hidden',
            borderRight: '0.15em solid currentColor',
        },
        fadeIn: {
            opacity: 0,
            animation: 'fadeIn 2s forwards',
        },
        bounce: {
            display: 'inline-block',
        },
        glitch: {
            position: 'relative',
            animation: 'glitch 1s linear infinite',
        },
        rainbow: {
            background: 'linear-gradient(to right, #6666ff, #0099ff , #00ff00, #ff3399, #6666ff)',
            backgroundSize: '400%',
            backgroundClip: 'text',
            WebkitBackgroundClip: 'text',
            WebkitTextFillColor: 'transparent',
            animation: 'rainbow 8s ease infinite',
        }
    };

    const effects = {
        typewriter: (text) => {
            const [displayText, setDisplayText] = useState('');
            const [isAnimating, setIsAnimating] = useState(true);

            useEffect(() => {
                if (!text) {
                    setIsAnimating(false);
                    return;
                }

                const startAnimation = () => {
                    let i = 0;
                    setDisplayText('');
                    setIsAnimating(true);

                    const timer = setInterval(() => {
                        try {
                            if (i < text.length) {
                                setDisplayText(prev => prev + text[i]);
                                i++;
                            } else {
                                setIsAnimating(false);
                                if (defaultConfig.loop) {
                                    setTimeout(startAnimation, defaultConfig.delay);
                                }
                                clearInterval(timer);
                            }
                        } catch (error) {
                            console.error('Animation error:', error);
                            clearInterval(timer);
                            setIsAnimating(false);
                        }
                    }, defaultConfig.speed);

                    return timer;
                };

                const timer = setTimeout(startAnimation, defaultConfig.delay);
                return () => clearTimeout(timer);
            }, [text, defaultConfig.loop, defaultConfig.speed, defaultConfig.delay]);

            return (
                <span
                    style={{
                        ...defaultStyles.base,
                        ...defaultStyles.typewriter,
                        ...styles,
                        borderRight: isAnimating ? '0.15em solid currentColor' : 'none'
                    }}
                    aria-label={text}
                >
                    {displayText}
                </span>
            );
        },

        fadeIn: (text) => (
            <span
                style={{
                    ...defaultStyles.base,
                    ...defaultStyles.fadeIn,
                    ...styles
                }}
            >
                {text}
            </span>
        ),

        bounce: (text) => (
            <span style={{ ...defaultStyles.base, ...styles }}>
                {text.split('').map((char, i) => (
                    <span
                        key={i}
                        style={{
                            ...defaultStyles.bounce,
                            animation: `bounce 0.5s ease infinite`,
                            animationDelay: `${i * 0.1}s`
                        }}
                    >
                        {char}
                    </span>
                ))}
            </span>
        ),

        glitch: (text) => (
            <span style={{ ...defaultStyles.base, ...defaultStyles.glitch, ...styles }}>
                {text}
                <span className="glitch-effect" data-text={text}></span>
            </span>
        ),

        rainbow: (text) => (
            <span style={{ ...defaultStyles.base, ...defaultStyles.rainbow, ...styles }}>
                {text}
            </span>
        )
    };

    useEffect(() => {
        const styleSheet = document.createElement('style');
        styleSheet.textContent = `
      @keyframes fadeIn {
        to { opacity: 1; }
      }
      @keyframes bounce {
        0%, 100% { transform: translateY(0); }
        50% { transform: translateY(-10px); }
      }
      @keyframes glitch {
        2%, 64% { transform: translate(2px,0) skew(0deg); }
        4%, 60% { transform: translate(-2px,0) skew(0deg); }
        62% { transform: translate(0,0) skew(5deg); }
      }
      @keyframes rainbow { 
        0% { background-position: 0% 50%; }
        50% { background-position: 100% 50%; }
        100% { background-position: 0% 50%; }
      }
    `;
        document.head.appendChild(styleSheet);
        return () => document.head.removeChild(styleSheet);
    }, []);

    const safeText = typeof text === 'string' ? text : String(text || '');
    const safeEffect = effects.hasOwnProperty(effect) ? effect : 'typewriter';

    return (
        <TextErrorBoundary>
            {effects[safeEffect](safeText)}
        </TextErrorBoundary>
    );
};

export default AnimatedText;