Text animation that transforms text by randomly cycling through characters before settling on the final content, creating an engaging cryptographic effect.
1'use client';
2import { type JSX, useEffect, useState } from 'react';
3import { motion, MotionProps } from 'motion/react';
4
5export type TextScrambleProps = {
6 children: string;
7 duration?: number;
8 speed?: number;
9 characterSet?: string;
10 as?: React.ElementType;
11 className?: string;
12 trigger?: boolean;
13 onScrambleComplete?: () => void;
14} & MotionProps;
15
16const defaultChars =
17 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
18
19export function TextScramble({
20 children,
21 duration = 0.8,
22 speed = 0.04,
23 characterSet = defaultChars,
24 className,
25 as: Component = 'p',
26 trigger = true,
27 onScrambleComplete,
28 ...props
29}: TextScrambleProps) {
30 const MotionComponent = motion.create(
31 Component as keyof JSX.IntrinsicElements
32 );
33 const [displayText, setDisplayText] = useState(children);
34 const [isAnimating, setIsAnimating] = useState(false);
35 const text = children;
36
37 const scramble = async () => {
38 if (isAnimating) return;
39 setIsAnimating(true);
40
41 const steps = duration / speed;
42 let step = 0;
43
44 const interval = setInterval(() => {
45 let scrambled = '';
46 const progress = step / steps;
47
48 for (let i = 0; i < text.length; i++) {
49 if (text[i] === ' ') {
50 scrambled += ' ';
51 continue;
52 }
53
54 if (progress * text.length > i) {
55 scrambled += text[i];
56 } else {
57 scrambled +=
58 characterSet[Math.floor(Math.random() * characterSet.length)];
59 }
60 }
61
62 setDisplayText(scrambled);
63 step++;
64
65 if (step > steps) {
66 clearInterval(interval);
67 setDisplayText(text);
68 setIsAnimating(false);
69 onScrambleComplete?.();
70 }
71 }, speed * 1000);
72 };
73
74 useEffect(() => {
75 if (!trigger) return;
76
77 scramble();
78 }, [trigger]);
79
80 return (
81 <MotionComponent className={className} {...props}>
82 {displayText}
83 </MotionComponent>
84 );
85}
86