@@ -12,7 +12,6 @@ function randomGlyph() {
1212interface ScrambleTextProps {
1313 text : string ;
1414 className ?: string ;
15- /** Delay in ms before starting the decode (useful for staggering) */
1615 delay ?: number ;
1716}
1817
@@ -22,26 +21,29 @@ export default function ScrambleText({ text, className = '', delay = 0 }: Scramb
2221 const [ display , setDisplay ] = useState ( text ) ;
2322 const hasPlayed = useRef ( false ) ;
2423 const isHovering = useRef ( false ) ;
24+ const activeInterval = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
25+ const activeTimeout = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
26+
27+ const clearActive = useCallback ( ( ) => {
28+ if ( activeInterval . current ) { clearInterval ( activeInterval . current ) ; activeInterval . current = null ; }
29+ if ( activeTimeout . current ) { clearTimeout ( activeTimeout . current ) ; activeTimeout . current = null ; }
30+ } , [ ] ) ;
2531
2632 const scramble = useCallback ( ( targetText : string , startDelay : number ) => {
27- let cancelled = false ;
33+ clearActive ( ) ;
2834
2935 const chars = targetText . split ( '' ) ;
3036 const resolved = new Array ( chars . length ) . fill ( false ) ;
3137 const current = chars . map ( ( ch ) => ( ch === ' ' ? ' ' : randomGlyph ( ) ) ) ;
3238
33- // Immediately show scrambled state
34- setTimeout ( ( ) => {
35- if ( cancelled ) return ;
39+ activeTimeout . current = setTimeout ( ( ) => {
40+ activeTimeout . current = null ;
3641 setDisplay ( current . join ( '' ) ) ;
3742
3843 let charIndex = 0 ;
3944 let cycleCount = 0 ;
4045
41- const interval = setInterval ( ( ) => {
42- if ( cancelled ) { clearInterval ( interval ) ; return ; }
43-
44- // Cycle random glyphs for unresolved characters
46+ activeInterval . current = setInterval ( ( ) => {
4547 for ( let i = charIndex ; i < chars . length ; i ++ ) {
4648 if ( ! resolved [ i ] && chars [ i ] !== ' ' ) {
4749 current [ i ] = randomGlyph ( ) ;
@@ -50,13 +52,11 @@ export default function ScrambleText({ text, className = '', delay = 0 }: Scramb
5052
5153 cycleCount ++ ;
5254 if ( cycleCount >= CYCLES ) {
53- // Lock in the next character
5455 resolved [ charIndex ] = true ;
5556 current [ charIndex ] = chars [ charIndex ] ;
5657 charIndex ++ ;
5758 cycleCount = 0 ;
5859
59- // Skip spaces
6060 while ( charIndex < chars . length && chars [ charIndex ] === ' ' ) {
6161 resolved [ charIndex ] = true ;
6262 current [ charIndex ] = ' ' ;
@@ -67,16 +67,18 @@ export default function ScrambleText({ text, className = '', delay = 0 }: Scramb
6767 setDisplay ( current . join ( '' ) ) ;
6868
6969 if ( charIndex >= chars . length ) {
70- clearInterval ( interval ) ;
70+ clearActive ( ) ;
7171 setDisplay ( targetText ) ;
7272 }
7373 } , TICK_MS ) ;
7474 } , startDelay ) ;
7575
76- return ( ) => { cancelled = true ; } ;
77- } , [ ] ) ;
76+ return ( ) => {
77+ clearActive ( ) ;
78+ setDisplay ( targetText ) ;
79+ } ;
80+ } , [ clearActive ] ) ;
7881
79- // Scramble on scroll into view
8082 useEffect ( ( ) => {
8183 if ( isInView && ! hasPlayed . current ) {
8284 hasPlayed . current = true ;
@@ -85,27 +87,25 @@ export default function ScrambleText({ text, className = '', delay = 0 }: Scramb
8587 }
8688 } , [ isInView , text , delay , scramble ] ) ;
8789
88- // Re-scramble on hover (ripple from center outward)
8990 const handleMouseEnter = useCallback ( ( ) => {
9091 if ( isHovering . current ) return ;
9192 isHovering . current = true ;
93+ clearActive ( ) ;
9294
9395 const chars = text . split ( '' ) ;
9496 const current = text . split ( '' ) ;
9597 const mid = Math . floor ( chars . length / 2 ) ;
9698
97- // Calculate distance from center for each character
9899 const distances = chars . map ( ( _ , i ) => Math . abs ( i - mid ) ) ;
99100 const maxDist = Math . max ( ...distances ) ;
100101 const resolved = new Array ( chars . length ) . fill ( false ) ;
101102
102- // Mark spaces as already resolved
103103 chars . forEach ( ( ch , i ) => { if ( ch === ' ' ) resolved [ i ] = true ; } ) ;
104104
105105 let tick = 0 ;
106106 const totalTicks = ( maxDist + 1 ) * CYCLES + CYCLES ;
107107
108- const interval = setInterval ( ( ) => {
108+ activeInterval . current = setInterval ( ( ) => {
109109 tick ++ ;
110110
111111 for ( let i = 0 ; i < chars . length ; i ++ ) {
@@ -122,12 +122,16 @@ export default function ScrambleText({ text, className = '', delay = 0 }: Scramb
122122 setDisplay ( current . join ( '' ) ) ;
123123
124124 if ( tick >= totalTicks || resolved . every ( Boolean ) ) {
125- clearInterval ( interval ) ;
125+ clearActive ( ) ;
126126 setDisplay ( text ) ;
127127 isHovering . current = false ;
128128 }
129129 } , TICK_MS ) ;
130- } , [ text ] ) ;
130+ } , [ text , clearActive ] ) ;
131+
132+ useEffect ( ( ) => {
133+ return ( ) => clearActive ( ) ;
134+ } , [ clearActive ] ) ;
131135
132136 return (
133137 < span
0 commit comments