@@ -67,6 +67,19 @@ export default function FeatureCarousel() {
6767 const requestRef = useRef < number > ( ) ;
6868 const lastTimestampRef = useRef < number > ( ) ;
6969 const timerRef = useRef < NodeJS . Timeout > ( ) ;
70+ const hoverTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
71+
72+ // Refs for Loop State to avoid Re-renders/Effect teardown
73+ const isPausedRef = useRef ( false ) ;
74+ const isSnappingRef = useRef ( false ) ;
75+ const isDraggingRef = useRef ( false ) ;
76+ const selectedFeatureRef = useRef < string | null > ( null ) ;
77+
78+ // Sync Refs
79+ useEffect ( ( ) => { isPausedRef . current = isPaused ; } , [ isPaused ] ) ;
80+ useEffect ( ( ) => { isSnappingRef . current = isSnapping ; } , [ isSnapping ] ) ;
81+ useEffect ( ( ) => { isDraggingRef . current = isDragging ; } , [ isDragging ] ) ;
82+ useEffect ( ( ) => { selectedFeatureRef . current = selectedFeature ; } , [ selectedFeature ] ) ;
7083
7184 // Mobile detection
7285 useEffect ( ( ) => {
@@ -95,26 +108,25 @@ export default function FeatureCarousel() {
95108 } , [ totalSetWidth ] ) ;
96109
97110 // Snapping Logic - Precision Centering
98- const snapToNearest = useCallback ( ( ) => {
111+ const snapToIndex = useCallback ( ( index : number ) => {
99112 if ( ! containerRef . current ) return ;
100113 setIsSnapping ( true ) ;
101- const currentX = xRaw . get ( ) ;
102114 const viewportWidth = containerRef . current . offsetWidth ;
103115 const center = viewportWidth / 2 ;
104116
105- const bestIndex = Math . round ( ( center - currentX - ( cardWidth / 2 ) ) / itemWidth ) ;
106- const targetX = center - ( bestIndex * itemWidth ) - ( cardWidth / 2 ) ;
117+ const targetX = center - ( index * itemWidth ) - ( cardWidth / 2 ) ;
107118
108- setCenteredIndex ( bestIndex ) ;
119+ setCenteredIndex ( index ) ;
109120
110121 animate ( xRaw , targetX , {
111- type : "tween" ,
112- ease : [ 0.25 , 1 , 0.5 , 1 ] , // Custom smooth ease
113- duration : 0.6 ,
122+ type : "spring" ,
123+ stiffness : 300 ,
124+ damping : 30 ,
125+ mass : 1 ,
114126 onComplete : ( ) => {
115127 const finalX = xRaw . get ( ) ;
116128 const wrappedX = wrapAround ( finalX ) ;
117- if ( wrappedX !== finalX ) {
129+ if ( Math . abs ( wrappedX - finalX ) > 1 ) { // Only reset if difference is significant
118130 xRaw . set ( wrappedX ) ;
119131 const newIndex = Math . round ( ( center - wrappedX - ( cardWidth / 2 ) ) / itemWidth ) ;
120132 setCenteredIndex ( newIndex ) ;
@@ -124,37 +136,29 @@ export default function FeatureCarousel() {
124136 } ) ;
125137 } , [ xRaw , itemWidth , cardWidth , wrapAround ] ) ;
126138
139+ const snapToNearest = useCallback ( ( ) => {
140+ if ( ! containerRef . current ) return ;
141+ const currentX = xRaw . get ( ) ;
142+ const viewportWidth = containerRef . current . offsetWidth ;
143+ const center = viewportWidth / 2 ;
144+ const bestIndex = Math . round ( ( center - currentX - ( cardWidth / 2 ) ) / itemWidth ) ;
145+ snapToIndex ( bestIndex ) ;
146+ } , [ xRaw , itemWidth , cardWidth , snapToIndex ] ) ;
147+
127148 const handleManualMove = useCallback ( ( direction : 'left' | 'right' ) => {
128149 if ( ! containerRef . current ) return ;
129- setIsSnapping ( true ) ;
130150 const currentX = xRaw . get ( ) ;
131151 const viewportWidth = containerRef . current . offsetWidth ;
132152 const center = viewportWidth / 2 ;
133153
134154 // Base the next move on the current visual position to ensure accuracy
135155 const currentIdx = Math . round ( ( center - currentX - ( cardWidth / 2 ) ) / itemWidth ) ;
136156 const nextIndex = direction === 'left' ? currentIdx - 1 : currentIdx + 1 ;
137- const targetX = center - ( nextIndex * itemWidth ) - ( cardWidth / 2 ) ;
138-
139- setCenteredIndex ( nextIndex ) ;
140- animate ( xRaw , targetX , {
141- type : "tween" ,
142- ease : [ 0.25 , 1 , 0.5 , 1 ] ,
143- duration : 0.6 ,
144- onComplete : ( ) => {
145- const finalX = xRaw . get ( ) ;
146- const wrappedX = wrapAround ( finalX ) ;
147- if ( wrappedX !== finalX ) {
148- xRaw . set ( wrappedX ) ;
149- const newIndex = Math . round ( ( center - wrappedX - ( cardWidth / 2 ) ) / itemWidth ) ;
150- setCenteredIndex ( newIndex ) ;
151- }
152- setIsSnapping ( false ) ;
153- }
154- } ) ;
155- } , [ xRaw , itemWidth , cardWidth , wrapAround ] ) ;
157+ snapToIndex ( nextIndex ) ;
158+ } , [ xRaw , itemWidth , cardWidth , snapToIndex ] ) ;
156159
157160 // Animation Loop (Desktop Only)
161+ // Animation Loop (Desktop Only) - Optimized with Refs
158162 useEffect ( ( ) => {
159163 if ( isMobile ) return ;
160164
@@ -163,7 +167,8 @@ export default function FeatureCarousel() {
163167 const deltaTime = timestamp - lastTimestampRef . current ;
164168 lastTimestampRef . current = timestamp ;
165169
166- if ( ! isPaused && ! selectedFeature && ! isSnapping && ! isDragging ) {
170+ // Read directly from refs to avoid effect dependencies
171+ if ( ! isPausedRef . current && ! selectedFeatureRef . current && ! isSnappingRef . current && ! isDraggingRef . current ) {
167172 const currentX = xRaw . get ( ) ;
168173 const nextX = currentX - ( speed * deltaTime ) / 1000 ;
169174 xRaw . set ( wrapAround ( nextX ) ) ;
@@ -175,7 +180,8 @@ export default function FeatureCarousel() {
175180 return ( ) => {
176181 if ( requestRef . current ) cancelAnimationFrame ( requestRef . current ) ;
177182 } ;
178- } , [ isPaused , selectedFeature , isSnapping , isDragging , wrapAround , xRaw , isMobile ] ) ;
183+ // Dependencies are minimal: only re-run if mobile state or structural layout changes
184+ } , [ isMobile , wrapAround , xRaw ] ) ;
179185
180186 // Auto-timer for Mobile
181187 useEffect ( ( ) => {
@@ -217,12 +223,17 @@ export default function FeatureCarousel() {
217223 if ( isMobile ) return ;
218224 setIsPaused ( true ) ;
219225 snapToNearest ( ) ;
220- } , [ snapToNearest , isMobile ] ) ;
226+ } , [ isMobile , snapToNearest ] ) ;
221227
222228 const handleMouseLeaveParent = useCallback ( ( ) => {
223229 if ( isMobile ) return ;
230+ if ( hoverTimeoutRef . current ) {
231+ clearTimeout ( hoverTimeoutRef . current ) ;
232+ hoverTimeoutRef . current = null ;
233+ }
224234 setIsPaused ( false ) ;
225235 setCenteredIndex ( null ) ;
236+ setHoveredCard ( null ) ;
226237 lastTimestampRef . current = undefined ;
227238 } , [ isMobile ] ) ;
228239
@@ -286,11 +297,30 @@ export default function FeatureCarousel() {
286297 ease : "easeOut" ,
287298 duration : 0.4
288299 } }
289- onMouseEnter = { ( ) => ! isMobile && setHoveredCard ( idx ) }
290- onMouseLeave = { ( ) => ! isMobile && setHoveredCard ( null ) }
300+ onMouseEnter = { ( ) => {
301+ if ( ! isMobile ) {
302+ setHoveredCard ( idx ) ;
303+ // Debounce slightly to prevent erratic behavior, but keep it snappy
304+ if ( hoverTimeoutRef . current ) clearTimeout ( hoverTimeoutRef . current ) ;
305+ hoverTimeoutRef . current = setTimeout ( ( ) => {
306+ if ( ! isDragging ) {
307+ snapToIndex ( idx ) ;
308+ }
309+ } , 50 ) ; // Reduced from 150ms to 50ms for faster response
310+ }
311+ } }
312+ onMouseLeave = { ( ) => {
313+ if ( ! isMobile ) {
314+ setHoveredCard ( null ) ;
315+ if ( hoverTimeoutRef . current ) {
316+ clearTimeout ( hoverTimeoutRef . current ) ;
317+ hoverTimeoutRef . current = null ;
318+ }
319+ }
320+ } }
291321 whileHover = { {
292322 scale : isMobile ? 1.0 : ( isCentered ? 1.05 : 0.98 ) ,
293- transition : { type : "tween " , ease : "easeOut" , duration : 0.2 }
323+ transition : { type : "spring " , stiffness : 400 , damping : 25 }
294324 } }
295325 onClick = { ( ) => setSelectedFeature ( feature . key ) }
296326 className = { `relative shrink-0 rounded-[2.5rem] md:rounded-[3rem] p-6 md:p-10 overflow-hidden cursor-pointer bg-slate-950/40 border transition-all duration-300 ${ isHovered || ( isMobile && isCentered )
@@ -300,7 +330,7 @@ export default function FeatureCarousel() {
300330 style = { {
301331 width : cardWidth ,
302332 height : isMobile ? 380 : 440 ,
303- backgroundColor : ( isHovered || isMobile ) ? `${ featureColor } 08` : 'rgba(255,255,255,0.03 )' ,
333+ backgroundColor : ( isHovered || isMobile ) ? `${ featureColor } 08` : 'rgba(30, 41, 59, 0.4 )' , // More solid, no blur
304334 borderColor : ( isHovered || isMobile ) ? `${ featureColor } 99` : 'rgba(255,255,255,0.05)' ,
305335 boxShadow : ( isHovered || isMobile ) ? `0 25px 50px -12px ${ featureColor } 55` : 'none' ,
306336 [ '--tw-ring-color' as any ] : ( isHovered || ( isMobile && isCentered ) ) ? `${ featureColor } 44` : 'transparent' ,
@@ -348,55 +378,65 @@ export default function FeatureCarousel() {
348378 </ motion . div >
349379 </ div >
350380
351- { /* Glow Overlay */ }
381+ { /* Simplified Glow Overlay */ }
352382 < div
353383 className = "absolute inset-0 opacity-0 group-hover:opacity-10 transition-opacity duration-700 pointer-events-none"
354- style = { { background : `radial-gradient(circle at 50% 100%, ${ featureColor } , transparent)` } }
384+ style = { {
385+ background : isHovered ? `radial-gradient(circle at 50% 100%, ${ featureColor } , transparent)` : 'none'
386+ } }
355387 />
356388 </ motion . div >
357389 ) ;
358390 } ) }
359391 </ motion . div >
360392 </ div >
361393
362- { /* Navigation Arrows */ }
363- < AnimatePresence >
364- { ( isPaused || isMobile ) && ! selectedFeature && (
365- < div className = "absolute inset-0 pointer-events-none z-50 flex items-center justify-between px-4 md:justify-center md:px-0" >
366- < div className = { `relative ${ isMobile ? 'flex w-full justify-between items-center h-full' : 'w-[300px] h-[440px]' } ` } >
367- < motion . button
368- initial = { { opacity : 0 , scale : 0.8 } }
369- animate = { { opacity : 1 , scale : 1 , x : isMobile ? 0 : - 85 } }
370- exit = { { opacity : 0 , scale : 0.8 } }
371- onClick = { ( e ) => {
372- e . preventDefault ( ) ;
373- e . stopPropagation ( ) ;
374- handleManualMove ( 'left' ) ;
375- } }
376- className = { `${ isMobile ? 'relative' : 'absolute left-0 top-1/2 -translate-y-1/2 -translate-x-full'
377- } w-10 h-10 md:w-12 md:h-12 rounded-xl bg-slate-900/90 backdrop-blur-md border border-white/20 flex items-center justify-center text-white hover:text-white hover:bg-blue-600 hover:border-blue-400 transition-all shadow-2xl pointer-events-auto active:scale-90`}
378- >
379- < ChevronLeft size = { isMobile ? 24 : 28 } />
380- </ motion . button >
381-
382- < motion . button
383- initial = { { opacity : 0 , scale : 0.8 } }
384- animate = { { opacity : 1 , scale : 1 , x : isMobile ? 0 : 85 } }
385- exit = { { opacity : 0 , scale : 0.8 } }
386- onClick = { ( e ) => {
387- e . preventDefault ( ) ;
388- e . stopPropagation ( ) ;
389- handleManualMove ( 'right' ) ;
390- } }
391- className = { `${ isMobile ? 'relative' : 'absolute right-0 top-1/2 -translate-y-1/2 translate-x-full'
392- } w-10 h-10 md:w-12 md:h-12 rounded-xl bg-slate-900/90 backdrop-blur-md border border-white/20 flex items-center justify-center text-white hover:text-white hover:bg-blue-600 hover:border-blue-400 transition-all shadow-2xl pointer-events-auto active:scale-90`}
393- >
394- < ChevronRight size = { isMobile ? 24 : 28 } />
395- </ motion . button >
396- </ div >
397- </ div >
398- ) }
399- </ AnimatePresence >
394+ { /* Navigation Arrows - Optimized visibility without unmounting */ }
395+ < motion . div
396+ className = "absolute inset-0 pointer-events-none z-50 flex items-center justify-between px-4 md:justify-center md:px-0"
397+ animate = { {
398+ opacity : ( isPaused || isMobile ) && ! selectedFeature ? 1 : 0
399+ } }
400+ transition = { { duration : 0.3 } }
401+ >
402+ < div className = { `relative ${ isMobile ? 'flex w-full justify-between items-center h-full' : 'w-[450px] h-[440px]' } ` } >
403+ < motion . button
404+ initial = { { scale : 0.8 } }
405+ animate = { {
406+ scale : ( isPaused || isMobile ) ? 1 : 0.8 ,
407+ x : isMobile ? 0 : - 85 ,
408+ pointerEvents : ( isPaused || isMobile ) && ! selectedFeature ? 'auto' : 'none'
409+ } }
410+ onClick = { ( e ) => {
411+ e . preventDefault ( ) ;
412+ e . stopPropagation ( ) ;
413+ handleManualMove ( 'left' ) ;
414+ } }
415+ className = { `${ isMobile ? 'relative' : 'absolute left-0 top-1/2 -translate-y-1/2'
416+ } w-10 h-10 md:w-12 md:h-12 rounded-xl bg-slate-900 border border-white/20 flex items-center justify-center text-white hover:bg-blue-600 transition-all shadow-2xl active:scale-90`}
417+ >
418+ < ChevronLeft size = { isMobile ? 24 : 28 } />
419+ </ motion . button >
420+
421+ < motion . button
422+ initial = { { scale : 0.8 } }
423+ animate = { {
424+ scale : ( isPaused || isMobile ) ? 1 : 0.8 ,
425+ x : isMobile ? 0 : 85 ,
426+ pointerEvents : ( isPaused || isMobile ) && ! selectedFeature ? 'auto' : 'none'
427+ } }
428+ onClick = { ( e ) => {
429+ e . preventDefault ( ) ;
430+ e . stopPropagation ( ) ;
431+ handleManualMove ( 'right' ) ;
432+ } }
433+ className = { `${ isMobile ? 'relative' : 'absolute right-0 top-1/2 -translate-y-1/2'
434+ } w-10 h-10 md:w-12 md:h-12 rounded-xl bg-slate-900 border border-white/20 flex items-center justify-center text-white hover:bg-blue-600 transition-all shadow-2xl active:scale-90`}
435+ >
436+ < ChevronRight size = { isMobile ? 24 : 28 } />
437+ </ motion . button >
438+ </ div >
439+ </ motion . div >
400440 </ div >
401441
402442 { /* Feature Detailed Modal */ }
0 commit comments