Text Scramble Reveal

An interactive text component that scrambles characters on mouse proximity. Features customizable scramble characters, proximity detection, and smooth animations with pure React.

React
Animation
Interactive
Typography
Did you know that honey never spoils? Archaeologists have found pots of honey in ancient Egyptian tombs that are over 3,000 years old and still perfectly edible! This is because honeys unique chemical composition and low moisture content make it nearly impossible for bacteria and microorganisms to survive in it.Did you know that honey never spoils? Archaeologists have found pots of honey in ancient Egyptian tombs that are over 3,000 years old and still perfectly edible! This is because honeys unique chemical composition and low moisture content make it nearly impossible for bacteria and microorganisms to survive in it.Did you know that honey never spoils? Archaeologists have found pots of honey in ancient Egyptian tombs that are over 3,000 years old and still perfectly edible! This is because honeys unique chemical composition and low moisture content make it nearly impossible for bacteria and microorganisms to survive in it.Did you know that honey never spoils? Archaeologists have found pots of honey in ancient Egyptian tombs that are over 3,000 years old and still perfectly edible! This is because honeys unique chemical composition and low moisture content make it nearly impossible for bacteria and microorganisms to survive in it.

Using CLI

npx dimaac add TextScrambleReveal

Manual Installation

npm install react

lib/utils.ts

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
} 

components/ui/TextScrambleReveal.tsx

'use client';

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

interface TextScrambleRevealProps {
  children: React.ReactNode;
  className?: string;
  maxWidth?: string;
  fontSize?: string;
  lineHeight?: number;
  textColor?: string;
  scrambleColor?: string;
  scrambleChars?: string;
  proximityRadius?: number;
  backgroundColor?: string;
  fontFamily?: string;
}

const TextScrambleReveal: React.FC<TextScrambleRevealProps> = ({
  children,
  className = '',
  maxWidth = '800px',
  fontSize = '30px',
  lineHeight = 1.5,
  textColor = '#ffffffb4',
  scrambleColor = '#00c8ff',
  scrambleChars = '.:', 
  proximityRadius = 100,
  backgroundColor = 'transparent',
  fontFamily = '"Poppins", monospace',
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const textRef = useRef<HTMLDivElement>(null);
  const [chars, setChars] = useState<HTMLElement[]>([]);

  
  const scrambleChar = (element: HTMLElement, originalText: string) => {
    const scrambleCharsArray = scrambleChars.split('');
    let iterations = 0;
    const maxIterations = 8;

    const animate = () => {
      if (iterations < maxIterations) {
        const randomChar = scrambleCharsArray[Math.floor(Math.random() * scrambleCharsArray.length)];
        element.textContent = randomChar;
        iterations++;
        setTimeout(animate, 50);
      } else {
        element.textContent = originalText;
        element.classList.remove('scrambling');
        element.style.color = textColor;
      }
    };

    element.classList.add('scrambling');
    element.style.color = scrambleColor;
    console.log(scrambleColor)
    animate();
  };

  useEffect(() => {
    if (!textRef.current) return;

    const textContent = textRef.current.textContent || '';
    const charElements: HTMLElement[] = [];
    
    textRef.current.innerHTML = '';
    
    textContent.split('').forEach((char) => {
      const span = document.createElement('span');
      span.className = 'char';
      span.textContent = char;
      span.dataset.content = char;
      span.style.display = char === ' ' ? 'inline' : 'inline-block';
      textRef.current?.appendChild(span);
      charElements.push(span);
    });
    
    setChars(charElements);
  }, [children]);

  useEffect(() => {
    if (!containerRef.current || chars.length === 0) return;

    const handlePointerMove = (e: PointerEvent) => {
      chars.forEach(char => {
        const rect = char.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;
        const x = e.clientX - centerX;
        const y = e.clientY - centerY;
        const distance = Math.sqrt(x * x + y * y);
        
        if (distance < proximityRadius && !char.classList.contains('scrambling')) {
          const originalText = char.dataset.content || char.textContent || '';
          scrambleChar(char, originalText);
        }
      });
    };

    containerRef.current.addEventListener('pointermove', handlePointerMove);

    return () => {
      const container = containerRef.current;
      if (container) {
        container.removeEventListener('pointermove', handlePointerMove);
      }
    };
  }, [chars, proximityRadius, scrambleChar]);

  return (
    <div 
      ref={containerRef}
      className={`text-scramble ${className}`}
      style={{
        maxWidth,
        fontFamily,
        fontSize,
        lineHeight,
        color: textColor,
        background: backgroundColor,
        userSelect: 'none',
      }}
    >
      <div ref={textRef}>
        {typeof children === 'string' ? children : ''}
      </div>

    </div>
  );
};

export default TextScrambleReveal;