@@ -6,23 +6,26 @@ import { ChevronUp } from 'lucide-react';
66
77export default function ReturnToTop ( ) {
88 const [ isVisible , setIsVisible ] = useState ( false ) ;
9+ const [ scrollProgress , setScrollProgress ] = useState ( 0 ) ;
910
1011 useEffect ( ( ) => {
1112 const toggleVisibility = ( ) => {
12- // Show button when user is near the bottom of the page
1313 const scrollHeight = document . documentElement . scrollHeight ;
1414 const scrollTop = window . scrollY ;
1515 const clientHeight = window . innerHeight ;
1616
17- // Show when user is within 300px of the bottom
18- if ( scrollHeight - ( scrollTop + clientHeight ) < 300 ) {
19- setIsVisible ( true ) ;
20- } else {
21- setIsVisible ( false ) ;
22- }
17+ // Existing visibility logic
18+ setIsVisible ( scrollHeight - ( scrollTop + clientHeight ) < 300 ) ;
19+
20+ // Scroll progress calculation
21+ const progress = ( scrollTop / ( scrollHeight - clientHeight ) ) * 100 ;
22+
23+ setScrollProgress ( Math . min ( Math . max ( progress , 0 ) , 100 ) ) ;
2324 } ;
2425
25- window . addEventListener ( 'scroll' , toggleVisibility ) ;
26+ toggleVisibility ( ) ;
27+ window . addEventListener ( 'scroll' , toggleVisibility , { passive : true } ) ;
28+
2629 return ( ) => window . removeEventListener ( 'scroll' , toggleVisibility ) ;
2730 } , [ ] ) ;
2831
@@ -33,20 +36,67 @@ export default function ReturnToTop() {
3336 } ) ;
3437 } ;
3538
39+ // SVG ring values
40+ const radius = 24 ;
41+ const circumference = 2 * Math . PI * radius ;
42+
43+ const strokeDashoffset = circumference - ( scrollProgress / 100 ) * circumference ;
44+
3645 return (
3746 < AnimatePresence >
3847 { isVisible && (
39- < motion . button
48+ < motion . div
4049 initial = { { opacity : 0 , y : 20 } }
4150 animate = { { opacity : 1 , y : 0 } }
4251 exit = { { opacity : 0 , y : 20 } }
4352 transition = { { duration : 0.3 } }
44- onClick = { scrollToTop }
45- className = "fixed bottom-8 right-8 p-3 rounded-full border border-emerald-500/20 bg-white text-emerald-600 hover:bg-emerald-500 hover:text-white hover:border-emerald-500 dark:border-emerald-400/20 dark:bg-black dark:text-emerald-400 dark:hover:bg-emerald-400 dark:hover:text-black dark:hover:border-emerald-400 hover:scale-110 active:scale-95 shadow-[0_4px_20px_rgba(16,185,129,0.15)] dark:shadow-[0_4px_30px_rgba(16,185,129,0.3)] transition-all duration-300 z-50 flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00ffaa] focus-visible:ring-offset-2"
46- aria-label = "Return to top"
53+ className = "fixed bottom-8 right-8 z-50"
4754 >
48- < ChevronUp size = { 24 } />
49- </ motion . button >
55+ < div className = "relative flex items-center justify-center" >
56+ { /* Progress Ring */ }
57+ < svg className = "absolute -rotate-90" width = "64" height = "64" aria-hidden = "true" >
58+ { /* Track */ }
59+ < circle
60+ cx = "32"
61+ cy = "32"
62+ r = { radius }
63+ fill = "none"
64+ stroke = "currentColor"
65+ strokeWidth = "3"
66+ className = "text-emerald-500/15 dark:text-emerald-400/15"
67+ />
68+
69+ { /* Progress */ }
70+ < circle
71+ cx = "32"
72+ cy = "32"
73+ r = { radius }
74+ fill = "none"
75+ stroke = "currentColor"
76+ strokeWidth = "3"
77+ strokeLinecap = "round"
78+ className = "text-emerald-500 dark:text-emerald-400 transition-all duration-100"
79+ strokeDasharray = { circumference }
80+ strokeDashoffset = { strokeDashoffset }
81+ />
82+ </ svg >
83+
84+ { /* Existing Button */ }
85+ < button
86+ onClick = { scrollToTop }
87+ className = {
88+ 'relative p-3 rounded-full border border-emerald-500/20 bg-white ' +
89+ 'text-emerald-600 hover:bg-emerald-500 hover:text-white ' +
90+ 'hover:border-emerald-500 dark:border-emerald-400/20 ' +
91+ 'dark:bg-slate-900 dark:text-emerald-400 dark:hover:bg-emerald-600 ' +
92+ 'dark:hover:text-white transition-colors duration-200'
93+ }
94+ aria-label = "Return to top"
95+ >
96+ < ChevronUp size = { 24 } />
97+ </ button >
98+ </ div >
99+ </ motion . div >
50100 ) }
51101 </ AnimatePresence >
52102 ) ;
0 commit comments