'use client' import { useEffect, useRef, useCallback } from 'react' interface UseScrollPositionOptions { key: string enabled?: boolean debounceMs?: number } /** * Custom hook for preserving scroll position across navigation * Validates: Requirements 7.4 */ export function useScrollPosition(options: UseScrollPositionOptions) { const { key, enabled = true, debounceMs = 100 } = options const scrollPositionRef = useRef(0) const timeoutRef = useRef(null) const isRestoringRef = useRef(false) // Save scroll position to sessionStorage const saveScrollPosition = useCallback((position: number) => { if (!enabled) return try { sessionStorage.setItem(`scroll-${key}`, position.toString()) scrollPositionRef.current = position } catch (error) { console.warn('Failed to save scroll position:', error) } }, [key, enabled]) // Restore scroll position from sessionStorage const restoreScrollPosition = useCallback(() => { if (!enabled) return try { const savedPosition = sessionStorage.getItem(`scroll-${key}`) if (savedPosition !== null) { const position = parseInt(savedPosition, 10) if (!isNaN(position)) { isRestoringRef.current = true window.scrollTo(0, position) scrollPositionRef.current = position // Reset flag after a short delay setTimeout(() => { isRestoringRef.current = false }, 100) } } } catch (error) { console.warn('Failed to restore scroll position:', error) } }, [key, enabled]) // Clear saved scroll position const clearScrollPosition = useCallback(() => { try { sessionStorage.removeItem(`scroll-${key}`) scrollPositionRef.current = 0 } catch (error) { console.warn('Failed to clear scroll position:', error) } }, [key]) // Handle scroll events with debouncing useEffect(() => { if (!enabled) return const handleScroll = () => { // Don't save position if we're currently restoring if (isRestoringRef.current) return // Clear existing timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current) } // Debounce the save operation timeoutRef.current = setTimeout(() => { const position = window.scrollY || window.pageYOffset saveScrollPosition(position) }, debounceMs) } window.addEventListener('scroll', handleScroll, { passive: true }) return () => { window.removeEventListener('scroll', handleScroll) if (timeoutRef.current) { clearTimeout(timeoutRef.current) } } }, [enabled, debounceMs, saveScrollPosition]) // Restore scroll position on mount useEffect(() => { if (enabled) { // Use requestAnimationFrame to ensure DOM is ready requestAnimationFrame(() => { restoreScrollPosition() }) } }, [enabled, restoreScrollPosition]) return { saveScrollPosition, restoreScrollPosition, clearScrollPosition, currentPosition: scrollPositionRef.current } } /** * Hook for preserving scroll position within a specific container element */ export function useContainerScrollPosition(options: UseScrollPositionOptions) { const { key, enabled = true, debounceMs = 100 } = options const containerRef = useRef(null) const scrollPositionRef = useRef(0) const timeoutRef = useRef(null) const isRestoringRef = useRef(false) // Save scroll position const saveScrollPosition = useCallback((position: number) => { if (!enabled) return try { sessionStorage.setItem(`container-scroll-${key}`, position.toString()) scrollPositionRef.current = position } catch (error) { console.warn('Failed to save container scroll position:', error) } }, [key, enabled]) // Restore scroll position const restoreScrollPosition = useCallback(() => { if (!enabled || !containerRef.current) return try { const savedPosition = sessionStorage.getItem(`container-scroll-${key}`) if (savedPosition !== null) { const position = parseInt(savedPosition, 10) if (!isNaN(position)) { isRestoringRef.current = true containerRef.current.scrollTop = position scrollPositionRef.current = position setTimeout(() => { isRestoringRef.current = false }, 100) } } } catch (error) { console.warn('Failed to restore container scroll position:', error) } }, [key, enabled]) // Clear saved scroll position const clearScrollPosition = useCallback(() => { try { sessionStorage.removeItem(`container-scroll-${key}`) scrollPositionRef.current = 0 } catch (error) { console.warn('Failed to clear container scroll position:', error) } }, [key]) // Set up scroll listener on container useEffect(() => { if (!enabled || !containerRef.current) return const container = containerRef.current const handleScroll = () => { if (isRestoringRef.current) return if (timeoutRef.current) { clearTimeout(timeoutRef.current) } timeoutRef.current = setTimeout(() => { const position = container.scrollTop saveScrollPosition(position) }, debounceMs) } container.addEventListener('scroll', handleScroll, { passive: true }) return () => { container.removeEventListener('scroll', handleScroll) if (timeoutRef.current) { clearTimeout(timeoutRef.current) } } }, [enabled, debounceMs, saveScrollPosition]) return { containerRef, saveScrollPosition, restoreScrollPosition, clearScrollPosition, currentPosition: scrollPositionRef.current } }