GlitchButtonCRT.tsx (Glitch Implementation)
import { Button , ButtonProps } from "@mantine/core" ;
import { useState , useRef , useEffect } from "react" ;
import { useReducedMotion } from "@mantine/hooks" ;
import classes from "./GlitchButtonCRT.module.css" ;
interface GlitchButtonCRTProps extends ButtonProps {
text : string ;
}
const CHARS = "!@#$%^&*_+-=|;:<>?/" ;
export function GlitchButtonCRT ( { text , ... others } : GlitchButtonCRTProps ) {
const [ display , setDisplay ] = useState ( text ) ;
const [ isHovered , setIsHovered ] = useState ( false ) ;
const frameRef = useRef < number > ( 0 ) ;
const reduceMotion = useReducedMotion ( ) ;
useEffect ( ( ) => {
if ( reduceMotion || ! isHovered ) {
// eslint-disable-next-line react-hooks-extra/set-state-in-effect
setDisplay ( text ) ;
if ( frameRef . current ) cancelAnimationFrame ( frameRef . current ) ;
return ;
}
let iteration = 0 ;
let isActive = true ;
const animate = ( ) => {
if ( ! isActive ) return ;
setDisplay ( ( ) =>
text
. split ( "" )
. map ( ( char , i ) => {
if ( char === " " ) return " " ;
return i < iteration ? char : CHARS [ Math . floor ( Math . random ( ) * CHARS . length ) ] ;
} )
. join ( "" ) ,
) ;
if ( iteration < text . length ) {
iteration += 1 / 3 ; // Fast scramble
frameRef . current = requestAnimationFrame ( animate ) ;
}
} ;
frameRef . current = requestAnimationFrame ( animate ) ;
return ( ) => {
isActive = false ;
if ( frameRef . current ) cancelAnimationFrame ( frameRef . current ) ;
} ;
} , [ isHovered , text , reduceMotion ] ) ;
return (
< Button
className = { classes . crtButton }
onMouseEnter = { ( ) => setIsHovered ( true ) }
onMouseLeave = { ( ) => setIsHovered ( false ) }
{ ... others }
>
{ display }
< / Button >
) ;
}