Scramble Text

Hover triggers rapid random-character cycling before resolving to the real text

animationtextscramblehoverdecode
Preview

Source Code

"use client";

import { useCallback, useRef, useState } from "react";

interface ScrambleTextProps {
  text: string;
  scrambleChars?: string;
  speed?: number;
  revealDelay?: number;
  className?: string;
}

export default function ScrambleText({
  text,
  scrambleChars = "!@#$%&*<>[]{}/'",
  speed = 30,
  revealDelay = 50,
  className = "",
}: ScrambleTextProps) {
  const [display, setDisplay] = useState(text);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const isAnimatingRef = useRef(false);

  const scramble = useCallback(() => {
    if (isAnimatingRef.current) return;
    isAnimatingRef.current = true;

    let revealed = 0;
    const chars = text.split("");

    intervalRef.current = setInterval(() => {
      const result = chars.map((char, i) => {
        if (char === " ") return " ";
        if (i < revealed) return char;
        return scrambleChars[Math.floor(Math.random() * scrambleChars.length)];
      });
      setDisplay(result.join(""));

      // Progressively reveal characters
      if (Date.now() % revealDelay < speed) {
        revealed++;
      }

      if (revealed >= chars.length) {
        if (intervalRef.current) clearInterval(intervalRef.current);
        setDisplay(text);
        isAnimatingRef.current = false;
      }
    }, speed);
  }, [text, scrambleChars, speed, revealDelay]);

  const handleMouseEnter = () => {
    scramble();
  };

  return (
    <span
      onMouseEnter={handleMouseEnter}
      className={`inline-block cursor-default font-mono ${className}`}
    >
      {display}
    </span>
  );
}