'use client'; import { forwardRef, useEffect, useState, useRef, HTMLAttributes } from 'react'; export type DistortionMode = 'glitch' | 'wave' | 'scramble' | 'noise' | 'split' | 'blink' | 'drip'; export type IntensityLevel = 'mild' | 'medium' | 'intense'; export type TriggerType = 'none' | 'hover' | 'click' | 'mount' | 'continuous'; export interface TextDistorterProps extends HTMLAttributes { /** Text to distort */ children: string; /** Distortion mode */ mode?: DistortionMode; /** Intensity level */ intensity?: IntensityLevel; /** When to trigger distortion */ trigger?: TriggerType; /** Custom color for glitch effect */ glitchColor1?: string; /** Secondary glitch color */ glitchColor2?: string; /** Color variant */ variant?: 'default' | 'neon' | 'fire' | 'matrix' | 'corruption'; /** Size variant */ size?: 'default' | 'small' | 'large' | 'xl'; /** Scramble characters */ scrambleChars?: string; /** Distortion speed (ms) */ speed?: number; /** Wave delay between characters (ms) */ waveDelay?: number; } const DEFAULT_SCRAMBLE_CHARS = '!<>-_\\/[]{}—=+*^?#________'; const variantColors = { default: { color1: '#ff00ff', color2: '#00ffff' }, neon: { color1: '#ff00ff', color2: '#00ffff' }, fire: { color1: '#ff4400', color2: '#ffaa00' }, matrix: { color1: '#00ff00', color2: '#003300' }, corruption: { color1: '#ff0000', color2: '#ffffff' }, }; const intensityClasses = { mild: { noiseIntensity: 0.3, animDuration: '2s' }, medium: { noiseIntensity: 0.5, animDuration: '1s' }, intense: { noiseIntensity: 0.8, animDuration: '0.5s' }, }; const sizeClasses = { small: 'text-xs', default: 'text-sm', large: 'text-xl', xl: 'text-2xl', }; export const TextDistorter = forwardRef( ( { children, mode = 'glitch', intensity = 'medium', trigger = 'continuous', glitchColor1, glitchColor2, variant = 'default', size = 'default', scrambleChars = DEFAULT_SCRAMBLE_CHARS, speed = 50, waveDelay = 50, className = '', ...props }, ref ) => { const [displayText, setDisplayText] = useState(children); const [isDistorting, setIsDistorting] = useState(trigger === 'continuous' || trigger === 'mount'); const scrambleTimeoutRef = useRef | null>(null); const colors = glitchColor1 && glitchColor2 ? { color1: glitchColor1, color2: glitchColor2 } : variantColors[variant]; const intensityConfig = intensityClasses[intensity]; const scrambleText = (targetText: string, duration: number) => { const startTime = Date.now(); const length = targetText.length; const update = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); let newText = ''; for (let i = 0; i < length; i++) { const charProgress = Math.max(0, progress * length - i * 0.5); if (charProgress >= 1 || Math.random() > charProgress) { newText += targetText[i]; } else { newText += scrambleChars[Math.floor(Math.random() * scrambleChars.length)]; } } setDisplayText(newText); if (progress < 1) { scrambleTimeoutRef.current = setTimeout(update, speed); } else { setDisplayText(targetText); } }; update(); }; useEffect(() => { if (trigger === 'mount') { if (mode === 'scramble') { scrambleText(children, 1000); } const timer = setTimeout(() => setIsDistorting(false), 2000); return () => clearTimeout(timer); } }, [children, trigger, mode]); useEffect(() => { if (trigger === 'continuous' && mode === 'scramble') { const interval = setInterval(() => { scrambleText(children, 500); }, 3000); return () => clearInterval(interval); } }, [children, trigger, mode]); const handleInteraction = () => { if (trigger === 'hover' || trigger === 'click') { if (mode === 'scramble') { scrambleText(children, 800); } setIsDistorting(true); setTimeout(() => setIsDistorting(false), 1000); } }; const renderChars = () => { return displayText.split('').map((char, index) => { const delay = mode === 'wave' ? index * waveDelay : 0; return ( {char === ' ' ? '\u00A0' : char} ); }); }; const shouldUseChars = mode === 'wave' || mode === 'split' || mode === 'blink'; const getModeClasses = () => { switch (mode) { case 'glitch': return 'relative animate-[glitchSkew_1s_infinite_linear_alternate]'; case 'wave': return 'inline-flex'; case 'scramble': return `font-mono ${isDistorting ? 'animate-[scrambleShake_0.1s_infinite]' : ''}`; case 'noise': return 'relative'; case 'split': return 'relative'; case 'blink': return ''; case 'drip': return 'relative overflow-visible'; default: return ''; } }; return ( {shouldUseChars ? renderChars() : displayText} {/* Glitch layers */} {mode === 'glitch' && ( <> {children} {children} )} {/* Noise overlay */} {mode === 'noise' && ( )} {/* Drip effect */} {mode === 'drip' && ( )} {/* Styles for animations */} ); } ); TextDistorter.displayName = 'TextDistorter'; export default TextDistorter;