{"version":3,"file":"index.cjs","names":["StorageManager","getHistoryKey","hasHash","pruneScrollPositionMap"],"sources":["../../../src/hooks/useScrollRestoration/index.ts"],"sourcesContent":["import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\nimport { useCallback, useRef } from 'react';\nimport { StorageManager, isWindow } from '@modern-kit/utils';\nimport {\n  pruneScrollPositionMap,\n  getHistoryKey,\n  hasHash,\n} from './useScrollRestoration.utils';\nimport { usePreventBrowserScrollRestoration } from '../usePreventBrowserScrollRestoration';\n\ninterface UseScrollRestorationOptions {\n  id?: string;\n  enabled?: boolean;\n  behavior?: ScrollOptions['behavior'];\n  retry?: number;\n}\n\ninterface UseScrollRestorationReturnType<T extends HTMLElement> {\n  ref: React.RefObject<T | null>;\n}\n\nconst STORAGE_KEY = 'mk-scroll-restoration';\nconst MAX_ENTRIES = 100;\nconst RETRY_TIME_INTERVAL = 100;\n\nconst sessionStorage = new StorageManager<{\n  [STORAGE_KEY]: Record<string, number>;\n}>('sessionStorage');\n\n/**\n * @description 브라우저 또는 특정 엘리먼트의 이전 스크롤 위치를 저장하고 복원하는 커스텀 훅입니다.\n *\n * @remarks\n * - 콘텐츠가 비동기적으로 로드되는 경우 혹은 이미지 로드로 인해 저장된 스크롤 위치보다 페이지 높이가 작을 수 있습니다.\n * - 이런 경우 자동으로 재시도하며, `지수 백오프(exponential backoff)` 방식으로 재시도 간격이 증가합니다.\n * - 재시도 간격: 100ms → 200ms → 400ms → 800ms → 1600ms\n * - 기본 최대 재시도 횟수는 `5회`이며, `retry` 옵션으로 조정할 수 있습니다.\n *\n * @remarks\n * - 한 컴포넌트 내에서 여러 번 훅을 사용할 경우, 각 인스턴스를 구분하려면 `id` 옵션을 명시적으로 부여하세요.\n * - 별도의 `id`를 지정하지 않으면, `ref`가 있는 경우 `'element'`, 없는 경우는 `'window'`로 기본값이 설정됩니다.\n * - id를 부여하지 않은 경우, 중복 key로 인해 스크롤 복원 동작이 정상적으로 동작하지 않을 수 있으니 주의가 필요합니다.\n *\n * @remarks\n * - 스크롤 위치 저장은 새로고침, 페이지 이동(뒤로/앞으로가기), 훅의 언마운트 시점에 이루어집니다.\n * - 따라서, 컴포넌트가 언마운트되지 않고 유지되는 구조에서 해당 훅을 호출 시 스크롤 복원 동작이 정상적으로 동작하지 않을 수 있으니 주의가 필요합니다. (예: Layout 컴포넌트)\n *\n * @remarks\n * - URL에 hash fragment(#section)가 있는 경우 스크롤 복원을 하지 않습니다.\n * - 이는 hash 스크롤이라는 명확한 사용자 의도를 존중하고, 브라우저의 표준 동작과 충돌을 방지하기 위함입니다.\n *\n * @template T - 스크롤 복원 대상 엘리먼트 타입\n * @param {UseScrollRestorationOptions} options - 스크롤 복원 옵션\n * @param {string} options.id - 스크롤 복원 식별자 (다중 인스턴스 사용 시 필수)\n * @param {boolean} [options.enabled=true] - 스크롤 복원 활성화 여부\n * @param {number} [options.retry=5] - 스크롤 복원 재시도 횟수 (지수 백오프 적용)\n * @param {ScrollOptions['behavior']} [options.behavior='instant'] - 스크롤 복원 동작 옵션\n * @returns {{ ref: React.RefObject<T> }} 스크롤 복원 대상 엘리먼트 ref 객체\n *\n * @example\n * ```ts\n * // window 스크롤 복원\n * useScrollRestoration();\n * ```\n *\n * @example\n * ```tsx\n * // 특정 엘리먼트 스크롤 복원\n * const { ref } = useScrollRestoration<HTMLDivElement>();\n * return <div ref={ref}>...</div>;\n * ```\n *\n * @example\n * ```ts\n * // 다중 인스턴스 스크롤 복원\n * useScrollRestoration();\n * const { ref: ref1 } = useScrollRestoration<HTMLDivElement>({ id: 'sidebar1' });\n * const { ref: ref2 } = useScrollRestoration<HTMLDivElement>({ id: 'sidebar2' });\n *\n * return (\n *   <div>\n *     <div ref={ref1}>...</div>\n *     <div ref={ref2}>...</div>\n *   </div>\n * );\n * ```\n */\nexport function useScrollRestoration<T extends HTMLElement>({\n  id,\n  enabled = true,\n  behavior = 'instant',\n  retry = 5,\n}: UseScrollRestorationOptions = {}): UseScrollRestorationReturnType<T> {\n  const ref = useRef<T | null>(null);\n  const isRestoredRef = useRef(false);\n  const historyKeyRef = useRef<string>('');\n  const retryCountRef = useRef(0);\n  const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const resetTimer = useCallback(() => {\n    if (retryTimeoutRef.current) {\n      clearTimeout(retryTimeoutRef.current);\n      retryTimeoutRef.current = null;\n    }\n  }, []);\n\n  const resetRetry = useCallback(() => {\n    retryCountRef.current = 0;\n    resetTimer();\n  }, [resetTimer]);\n\n  const ensureHistoryKey = useCallback((force = false) => {\n    if (force || !historyKeyRef.current) {\n      historyKeyRef.current = getHistoryKey();\n    }\n  }, []);\n\n  const getStorageKey = useCallback(() => {\n    const historyKey = historyKeyRef.current;\n    const instanceId = id ?? (ref.current ? 'element' : 'window');\n    return `${historyKey}-${instanceId}`;\n  }, [id]);\n\n  const saveScrollPosition = useCallback(() => {\n    if (hasHash() || !enabled || !historyKeyRef?.current) {\n      return;\n    }\n\n    const scrollTarget = ref.current || window;\n    const currentPos = isWindow(scrollTarget)\n      ? window.scrollY\n      : scrollTarget.scrollTop;\n\n    const prevStorageMap = sessionStorage.getItem(STORAGE_KEY) || {};\n    const cleanedMap = pruneScrollPositionMap(prevStorageMap, MAX_ENTRIES);\n\n    sessionStorage.setItem(STORAGE_KEY, {\n      ...cleanedMap,\n      [getStorageKey()]: currentPos,\n    });\n  }, [getStorageKey, enabled]);\n\n  const getCurrentScrollHeight = useCallback(() => {\n    const scrollTarget = ref.current || window;\n    return isWindow(scrollTarget)\n      ? document.documentElement.scrollHeight\n      : scrollTarget.scrollHeight;\n  }, []);\n\n  const executeScroll = useCallback(\n    (position: number) => {\n      const scrollTarget = ref.current || window;\n      const scrollToOptions: ScrollToOptions = {\n        top: position,\n        behavior,\n      };\n\n      requestAnimationFrame(() => {\n        if (isWindow(scrollTarget)) {\n          window.scrollTo(scrollToOptions);\n        } else {\n          scrollTarget.scrollTo(scrollToOptions);\n        }\n      });\n    },\n    [behavior]\n  );\n\n  const getSavedPosition = useCallback(() => {\n    const storageMap = sessionStorage.getItem(STORAGE_KEY);\n    if (!storageMap) return null;\n\n    return storageMap[getStorageKey()] ?? null;\n  }, [getStorageKey]);\n\n  const restoreScrollPosition = useCallback(() => {\n    if (hasHash()) {\n      resetRetry();\n      return;\n    }\n\n    if (!enabled || isRestoredRef.current) {\n      resetRetry();\n      return;\n    }\n\n    const scheduleRetry = () => {\n      if (retryTimeoutRef.current) {\n        resetTimer();\n      }\n\n      if (retryCountRef.current >= retry) {\n        resetRetry();\n        return;\n      }\n\n      retryCountRef.current++;\n\n      // 지수 백오프\n      const delay =\n        RETRY_TIME_INTERVAL * Math.pow(2, retryCountRef.current - 1);\n\n      retryTimeoutRef.current = setTimeout(restoreScrollPosition, delay);\n    };\n\n    ensureHistoryKey();\n    const savedPos = getSavedPosition();\n\n    if (savedPos == null) {\n      resetRetry();\n      return;\n    }\n    const currentScrollHeight = getCurrentScrollHeight();\n\n    if (currentScrollHeight >= savedPos) {\n      executeScroll(savedPos);\n      isRestoredRef.current = true;\n      resetRetry();\n    } else {\n      scheduleRetry();\n    }\n  }, [\n    enabled,\n    retry,\n    getSavedPosition,\n    ensureHistoryKey,\n    resetTimer,\n    resetRetry,\n    getCurrentScrollHeight,\n    executeScroll,\n  ]);\n\n  useIsomorphicLayoutEffect(() => {\n    restoreScrollPosition();\n\n    return () => {\n      saveScrollPosition();\n      resetRetry();\n    };\n  }, [saveScrollPosition, resetRetry, restoreScrollPosition]);\n\n  useIsomorphicLayoutEffect(() => {\n    if (!enabled) return;\n\n    const handleBeforeUnload = () => saveScrollPosition();\n    const handlePopstate = () => {\n      saveScrollPosition();\n\n      ensureHistoryKey(true);\n      isRestoredRef.current = false;\n\n      restoreScrollPosition();\n    };\n\n    window.addEventListener('beforeunload', handleBeforeUnload);\n    window.addEventListener('popstate', handlePopstate);\n\n    return () => {\n      window.removeEventListener('beforeunload', handleBeforeUnload);\n      window.removeEventListener('popstate', handlePopstate);\n    };\n  }, [saveScrollPosition, ensureHistoryKey, restoreScrollPosition, enabled]);\n\n  usePreventBrowserScrollRestoration();\n\n  return { ref };\n}\n"],"mappings":"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"}