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