@@ -6,6 +6,7 @@ import { formatDistanceToNow } from 'date-fns'
66import { findLibrary } from '~/libraries'
77
88const DISPLAY_DURATION = 7000 // 7 seconds in milliseconds
9+ const TRANSITION_DURATION = 500 // 0.5 seconds for crossfade
910
1011export function FeedTicker ( ) {
1112 // Fetch feed entries with default filters (major, minor releases, include prerelease)
@@ -24,7 +25,8 @@ export function FeedTicker() {
2425 } )
2526
2627 const [ currentIndex , setCurrentIndex ] = React . useState ( 0 )
27- const [ animationKey , setAnimationKey ] = React . useState ( 0 )
28+ const [ previousIndex , setPreviousIndex ] = React . useState < number | null > ( null )
29+ const [ isTransitioning , setIsTransitioning ] = React . useState ( false )
2830
2931 const entries = feedQuery . data ?. page || [ ]
3032
@@ -33,8 +35,15 @@ export function FeedTicker() {
3335
3436 // Rotate to next item after DISPLAY_DURATION
3537 const rotateTimeout = setTimeout ( ( ) => {
38+ setPreviousIndex ( currentIndex )
39+ setIsTransitioning ( true )
3640 setCurrentIndex ( ( prev ) => ( prev + 1 ) % entries . length )
37- setAnimationKey ( ( prev ) => prev + 1 )
41+
42+ // Clear previous after transition completes
43+ setTimeout ( ( ) => {
44+ setPreviousIndex ( null )
45+ setIsTransitioning ( false )
46+ } , TRANSITION_DURATION )
3847 } , DISPLAY_DURATION )
3948
4049 return ( ) => clearTimeout ( rotateTimeout )
@@ -44,7 +53,8 @@ export function FeedTicker() {
4453 React . useEffect ( ( ) => {
4554 if ( entries . length > 0 ) {
4655 setCurrentIndex ( 0 )
47- setAnimationKey ( 0 )
56+ setPreviousIndex ( null )
57+ setIsTransitioning ( false )
4858 }
4959 } , [ entries . length ] )
5060
@@ -54,6 +64,7 @@ export function FeedTicker() {
5464 }
5565
5666 const currentEntry = entries [ currentIndex ]
67+ const previousEntry = previousIndex !== null ? entries [ previousIndex ] : null
5768 if ( ! currentEntry ) return null
5869
5970 const renderEntry = ( entry : typeof currentEntry ) => {
@@ -161,43 +172,38 @@ export function FeedTicker() {
161172 width : '100%' ,
162173 } }
163174 >
164- { /* Progress bar - full height behind content */ }
165- < div className = "absolute inset-0 bg-gray-200/10 dark:bg-gray-700/10 rounded-lg overflow-hidden" >
166- < div
167- key = { animationKey }
168- className = "h-full w-full rounded-lg progress-gradient"
169- style = { {
170- transformOrigin : 'left' ,
171- animation : `progress ${ DISPLAY_DURATION } ms linear forwards` ,
172- } }
173- />
174- </ div >
175-
176- < div className = "relative z-10 h-full flex items-center" >
175+ < div className = "h-full flex items-center" >
176+ { /* Previous entry fading out */ }
177+ { previousEntry && isTransitioning && (
178+ < div
179+ key = { `prev-${ previousIndex } ` }
180+ className = "absolute inset-0 flex items-center animate-fade-out"
181+ >
182+ { renderEntry ( previousEntry ) }
183+ </ div >
184+ ) }
185+ { /* Current entry fading in */ }
177186 < div
178- key = { animationKey }
179- className = " absolute inset-0 flex items-center animate-fade-in"
187+ key = { `curr- ${ currentIndex } ` }
188+ className = { ` absolute inset-0 flex items-center ${ isTransitioning ? ' animate-fade-in' : '' } ` }
180189 >
181190 { renderEntry ( currentEntry ) }
182191 </ div >
183192 </ div >
184193 < style > { `
185- .progress-gradient {
186- background: linear-gradient(to right, transparent, rgb(209 213 219 / 0.3));
187- }
188- .dark .progress-gradient {
189- background: linear-gradient(to right, transparent, rgb(75 85 99 / 0.3));
190- }
191- @keyframes progress {
192- from { transform: scaleX(0); }
193- to { transform: scaleX(1); }
194- }
195194 @keyframes fadeIn {
196195 from { opacity: 0; }
197196 to { opacity: 1; }
198197 }
198+ @keyframes fadeOut {
199+ from { opacity: 1; }
200+ to { opacity: 0; }
201+ }
199202 .animate-fade-in {
200- animation: fadeIn 300ms ease-out forwards;
203+ animation: fadeIn 500ms ease-out forwards;
204+ }
205+ .animate-fade-out {
206+ animation: fadeOut 500ms ease-out forwards;
201207 }
202208 ` } </ style >
203209 </ div >
0 commit comments