index.tsx raw
1 import { Button } from '@/components/ui/button'
2 import { cn } from '@/lib/utils'
3 import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
4 import { useScreenSize } from '@/providers/ScreenSizeProvider'
5 import { hasBackgroundAudioAtom } from '@/services/media-manager.service'
6 import { useAtomValue } from 'jotai'
7 import { ChevronUp } from 'lucide-react'
8 import { useMemo } from 'react'
9
10 export default function ScrollToTopButton({
11 scrollAreaRef,
12 className
13 }: {
14 scrollAreaRef?: React.RefObject<HTMLDivElement>
15 className?: string
16 }) {
17 const { deepBrowsing, lastScrollTop } = useDeepBrowsing()
18 const { isSmallScreen } = useScreenSize()
19 const hasBackgroundAudio = useAtomValue(hasBackgroundAudioAtom)
20 const visible = useMemo(() => !deepBrowsing && lastScrollTop > 800, [deepBrowsing, lastScrollTop])
21
22 const handleScrollToTop = () => {
23 if (!scrollAreaRef) {
24 // scroll to top with custom animation
25 const startPosition = window.pageYOffset || document.documentElement.scrollTop
26 const duration = 500
27 const startTime = performance.now()
28
29 const easeInOutQuad = (t: number) => {
30 return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
31 }
32
33 const scroll = (currentTime: number) => {
34 const elapsed = currentTime - startTime
35 const progress = Math.min(elapsed / duration, 1)
36 const ease = easeInOutQuad(progress)
37
38 const position = startPosition * (1 - ease)
39 window.scrollTo(0, position)
40
41 if (progress < 1) {
42 requestAnimationFrame(scroll)
43 }
44 }
45
46 requestAnimationFrame(scroll)
47 return
48 }
49 scrollAreaRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
50 }
51
52 return (
53 <div
54 className={cn(
55 'sticky z-30 flex justify-end w-full pr-3 pointer-events-none transition-all duration-700',
56 visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4',
57 className
58 )}
59 style={{
60 bottom: isSmallScreen
61 ? `calc(env(safe-area-inset-bottom) + ${hasBackgroundAudio ? 7.25 : 3.85}rem)`
62 : `calc(env(safe-area-inset-bottom) + 0.85rem)`
63 }}
64 >
65 <Button
66 variant="secondary-2"
67 className="rounded-full size-12 p-0 hover:text-background pointer-events-auto disabled:pointer-events-none transition-all duration-200"
68 onClick={handleScrollToTop}
69 disabled={!visible}
70 >
71 <ChevronUp />
72 </Button>
73 </div>
74 )
75 }
76