@@ -53,33 +53,66 @@ export function HeroStat() {
5353 const [ count , setCount ] = useState ( COUNT_FROM )
5454
5555 useEffect ( ( ) => {
56- if ( window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ) {
56+ const media = window . matchMedia ( '(prefers-reduced-motion: reduce)' )
57+ let raf = 0
58+ let settleTimer : ReturnType < typeof setTimeout >
59+ let revealTimer : ReturnType < typeof setTimeout >
60+ let fillTimer : ReturnType < typeof setTimeout >
61+ let countTimer : ReturnType < typeof setTimeout >
62+
63+ const clearScheduled = ( ) => {
64+ clearTimeout ( settleTimer )
65+ clearTimeout ( revealTimer )
66+ clearTimeout ( fillTimer )
67+ clearTimeout ( countTimer )
68+ cancelAnimationFrame ( raf )
69+ }
70+
71+ const showSettled = ( ) => {
72+ clearScheduled ( )
5773 setSettled ( true )
5874 setRevealed ( true )
5975 setFilled ( true )
6076 setCount ( COUNT_TO )
61- return
6277 }
63- let raf = 0
64- const settleTimer = setTimeout ( ( ) => setSettled ( true ) , SETTLE_AT_MS )
65- const revealTimer = setTimeout ( ( ) => setRevealed ( true ) , REVEAL_AT_MS )
66- const fillTimer = setTimeout ( ( ) => setFilled ( true ) , FILL_AT_MS )
67- const countTimer = setTimeout ( ( ) => {
68- const start = performance . now ( )
69- const tick = ( now : number ) => {
70- const t = Math . min ( ( now - start ) / COUNT_DUR_MS , 1 )
71- const eased = 1 - ( 1 - t ) ** 3
72- setCount ( Math . round ( COUNT_FROM + ( COUNT_TO - COUNT_FROM ) * eased ) )
73- if ( t < 1 ) raf = requestAnimationFrame ( tick )
78+
79+ const runEntrance = ( ) => {
80+ setSettled ( false )
81+ setRevealed ( false )
82+ setFilled ( false )
83+ setCount ( COUNT_FROM )
84+ settleTimer = setTimeout ( ( ) => setSettled ( true ) , SETTLE_AT_MS )
85+ revealTimer = setTimeout ( ( ) => setRevealed ( true ) , REVEAL_AT_MS )
86+ fillTimer = setTimeout ( ( ) => setFilled ( true ) , FILL_AT_MS )
87+ countTimer = setTimeout ( ( ) => {
88+ const start = performance . now ( )
89+ const tick = ( now : number ) => {
90+ const t = Math . min ( ( now - start ) / COUNT_DUR_MS , 1 )
91+ const eased = 1 - ( 1 - t ) ** 3
92+ setCount ( Math . round ( COUNT_FROM + ( COUNT_TO - COUNT_FROM ) * eased ) )
93+ if ( t < 1 ) raf = requestAnimationFrame ( tick )
94+ }
95+ raf = requestAnimationFrame ( tick )
96+ } , REVEAL_AT_MS )
97+ }
98+
99+ // Re-synced on 'change' - not just on mount - so toggling the preference
100+ // mid-entrance snaps straight to the settled state instead of leaving the
101+ // stagger/count-up to keep running on its own clock.
102+ const syncMotionPreference = ( ) => {
103+ clearScheduled ( )
104+ if ( media . matches ) {
105+ showSettled ( )
106+ return
74107 }
75- raf = requestAnimationFrame ( tick )
76- } , REVEAL_AT_MS )
108+ runEntrance ( )
109+ }
110+
111+ syncMotionPreference ( )
112+ media . addEventListener ( 'change' , syncMotionPreference )
77113 return ( ) => {
78- clearTimeout ( settleTimer )
79- clearTimeout ( revealTimer )
80- clearTimeout ( fillTimer )
81- clearTimeout ( countTimer )
82- cancelAnimationFrame ( raf )
114+ media . removeEventListener ( 'change' , syncMotionPreference )
115+ clearScheduled ( )
83116 }
84117 } , [ ] )
85118
0 commit comments