{"version":3,"file":"useVirtualWindow.mjs","names":[],"sources":["../../../../package/common/virtualList/useVirtualWindow.ts"],"sourcesContent":["import React, { useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } from 'react';\n\nimport { buildOffsets, findStartIndex, getVisibleRange } from './virtualList.utils';\nimport { getScrollParent } from '../utils/scrollAncestors';\nimport { isWindowScroller, measureViewport, resolveScroller, scrollportBounds } from './scroller';\nimport type { Scroller, Viewport } from './scroller';\nimport type { VirtualListHandle, VirtualListScrollOptions, VirtualListScrollParent } from './virtualList.types';\n\n/**\n * Resolve a `rowGap` value to pixels for the offset math. Numbers and plain\n * `px`/numeric strings parse directly (no DOM, SSR/test-safe); any other CSS\n * length (`em`, `rem`, `%`, `var(...)`) is resolved via a hidden probe appended\n * to `contextEl` so it inherits the list's font-size and custom properties.\n */\nfunction resolveCssLength(value: number | string, contextEl: HTMLElement | null): number {\n  if (typeof value === 'number') return value;\n  const trimmed = value.trim();\n  if (trimmed === '') return 0;\n  const asNumber = Number(trimmed);\n  if (!Number.isNaN(asNumber)) return asNumber; // \"8\" → 8\n  const pxMatch = /^(-?\\d*\\.?\\d+)px$/.exec(trimmed);\n  if (pxMatch) return parseFloat(pxMatch[1]); // \"8px\" → 8\n  if (typeof document === 'undefined' || !contextEl) return 0;\n  const probe = document.createElement('div');\n  probe.style.cssText = `position:absolute;visibility:hidden;height:${trimmed};`;\n  contextEl.appendChild(probe);\n  const px = probe.getBoundingClientRect().height;\n  contextEl.removeChild(probe);\n  return Number.isFinite(px) ? px : 0;\n}\n\n/** No viewport holds this many rows at once, so rendering them all is a bug. */\nconst NOT_VIRTUALIZING_ROWS = 100;\n\ninterface UseVirtualWindowArgs<T> {\n  items: T[];\n  /** Stable key per row (already derived by the caller). */\n  keys: (string | number)[];\n  getItemKey?: (item: T, index: number) => string | number;\n  estimatedRowHeight: number;\n  rowGap: number | string;\n  overscan: number;\n  onLoadMore?: () => Promise<void>;\n  loadMoreThreshold: number;\n  /** Initial/filter load in progress — the body is replaced by a spinner. */\n  isLoading: boolean;\n  scrollParent?: VirtualListScrollParent;\n}\n\n/** Where the list is on screen, which is not its own box once it rides a scrollport. */\nexport interface ListViewportRect {\n  top: number;\n  bottom: number;\n  left: number;\n  right: number;\n}\n\n/** The list-space slice currently on screen, read live for keyboard navigation. */\nexport interface VisibleSlice {\n  top: number;\n  height: number;\n}\n\ninterface UseVirtualWindowResult {\n  rootRef: React.RefObject<HTMLDivElement | null>;\n  headerRef: React.RefObject<HTMLDivElement | null>;\n  /** Shared by every row to measure its height (read back via `data-index`). */\n  rowObserver: ResizeObserver | null;\n  /** Latest index → key mapping, for async consumers (e.g. the drag hook). */\n  keysRef: React.RefObject<(string | number)[]>;\n  offsets: number[];\n  totalHeight: number;\n  /** The list's visible slice, which is what decides which rows are rendered. */\n  containerHeight: number;\n  /** The whole scrollport, which is what a scroll target is aimed within. */\n  viewportHeight: number;\n  containerWidth: number;\n  startIndex: number;\n  endIndex: number;\n  showLoadMoreSpinner: boolean;\n  isWindowed: boolean;\n  getVisibleSlice: () => VisibleSlice;\n  getVisibleRect: () => ListViewportRect | null;\n  scrollBy: (dy: number) => void;\n  /** The scroll-related imperative methods, for the component to compose into\n   *  the forwarded handle alongside column controls. */\n  scrollApi: Omit<VirtualListHandle, 'recalculateColumns' | 'resetColumnWidths'>;\n}\n\n/**\n * The generic virtualization engine: viewport measurement, per-row height\n * caching, offset/visible-range computation, scroll coalescing, scroll\n * anchoring on height changes, infinite-load triggering, and the imperative\n * scroll handle. Mode-agnostic — knows nothing about columns, selection or\n * dragging.\n */\n/**\n * Loop guard for a jump re-aiming at its target. It normally ends because the\n * target stops moving, long before this; the cap is only there so a target that\n * can never be reached, the last row asked for with `align: 'start'`, can't\n * re-aim forever.\n */\nconst MAX_JUMP_CORRECTIONS = 20;\n\nexport function useVirtualWindow<T>({\n  items,\n  keys,\n  getItemKey,\n  estimatedRowHeight,\n  rowGap,\n  overscan,\n  onLoadMore,\n  loadMoreThreshold,\n  isLoading,\n  scrollParent,\n}: UseVirtualWindowArgs<T>): UseVirtualWindowResult {\n  const [scrollTop, setScrollTop] = useState(0);\n  const [containerHeight, setContainerHeight] = useState(0);\n  const [viewportHeight, setViewportHeight] = useState(0);\n  const [containerWidth, setContainerWidth] = useState(0);\n  // rowGap may be any CSS length; resolve it to px for the offset math. Seed with\n  // the synchronous fast path (number / px), then refine before paint for units\n  // that need the DOM (em/rem/%/var).\n  const [resolvedRowGap, setResolvedRowGap] = useState<number>(() => resolveCssLength(rowGap, null));\n  const [heightsMap, setHeightsMap] = useState<Map<string | number, number>>(new Map());\n  const [showLoadMoreSpinner, setShowLoadMoreSpinner] = useState(false);\n  const isLoadingMoreRef = useRef(false);\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const headerRef = useRef<HTMLDivElement>(null);\n  const prevOffsetsRef = useRef<number[]>([]);\n  const scrollRafRef = useRef<number | null>(null);\n  const keysRef = useRef<(string | number)[]>([]);\n\n  const isWindowed = scrollParent != null;\n  // The ref is what every callback reads, so none of them has to be rebuilt when\n  // the scroller resolves; the state exists only to re-run the listener effects.\n  const scrollerRef = useRef<Scroller | null>(null);\n  const nearestRef = useRef<Scroller | null>(null);\n  const [scroller, setScroller] = useState<Scroller | null>(null);\n  // No dependency list, and declared before every effect that measures: a\n  // getter's identity changes each render and the element it returns can appear\n  // at any commit. The setState cannot chain, because what it stores is stable\n  // across commits and it bails out on an equal one.\n  // oxlint-disable-next-line react-hooks/exhaustive-deps\n  useLayoutEffect(() => {\n    let next: Scroller | null;\n    if (scrollParent === 'nearest') {\n      // Walking the ancestors reads computed style and so forces a style recalc:\n      // resolve once, and again only if what it found has left the DOM.\n      const cached = nearestRef.current;\n      if (!cached || (!isWindowScroller(cached) && !cached.isConnected))\n        nearestRef.current = rootRef.current ? (getScrollParent(rootRef.current) ?? window) : null;\n      next = nearestRef.current;\n    } else {\n      next = resolveScroller(scrollParent);\n    }\n    scrollerRef.current = next;\n    setScroller((prev) => (prev === next ? prev : next));\n  });\n\n  // One ResizeObserver shared by every row, created once via a lazy state\n  // initializer so rows mounting in the same commit can register in their\n  // layout effect. Batching all entries into a single setState avoids a\n  // re-render per row when many rows enter the viewport at once (the main\n  // source of scroll jank). The row index is read back from `data-index`.\n  const [rowObserver] = useState<ResizeObserver | null>(() => {\n    if (typeof ResizeObserver === 'undefined') return null;\n    return new ResizeObserver((entries) => {\n      setHeightsMap((prev) => {\n        let next: Map<string | number, number> | null = null;\n        for (const entry of entries) {\n          const el = entry.target as HTMLElement;\n          const idx = Number(el.dataset.index);\n          if (Number.isNaN(idx)) continue;\n          // Resolve the row's stable key so the cache survives reorders/filters.\n          const key = keysRef.current[idx];\n          if (key === undefined) continue;\n          // Prefer the size the observer already computed — reading offsetHeight\n          // here would force a synchronous reflow on every measurement batch.\n          const box = entry.borderBoxSize?.[0];\n          const h = box ? Math.round(box.blockSize) : el.offsetHeight;\n          if (prev.get(key) === h) continue;\n          if (!next) next = new Map(prev);\n          next.set(key, h);\n        }\n        return next ?? prev;\n      });\n    });\n  });\n  useEffect(() => () => rowObserver?.disconnect(), [rowObserver]);\n\n  const applyViewport = useCallback((v: Viewport) => {\n    const top = Math.max(0, v.rel);\n    setScrollTop((prev) => (prev === top ? prev : top));\n    setContainerHeight((prev) => (prev === v.visibleHeight ? prev : v.visibleHeight));\n    setViewportHeight((prev) => (prev === v.viewportHeight ? prev : v.viewportHeight));\n    setContainerWidth((prev) => (prev === v.width ? prev : v.width));\n  }, []);\n\n  const syncViewport = useCallback(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    applyViewport(measureViewport(el, scrollerRef.current));\n  }, [applyViewport]);\n\n  const getVisibleSlice = useCallback((): VisibleSlice => {\n    const el = rootRef.current;\n    if (!el) return { top: 0, height: 0 };\n    const v = measureViewport(el, scrollerRef.current);\n    return { top: Math.max(0, v.rel), height: v.visibleHeight };\n  }, []);\n\n  const getVisibleRect = useCallback((): ListViewportRect | null => {\n    const el = rootRef.current;\n    if (!el) return null;\n    const rect = el.getBoundingClientRect();\n    const scrollerNow = scrollerRef.current;\n    if (!scrollerNow) return { top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right };\n    const { top, height } = scrollportBounds(scrollerNow);\n    return {\n      top: Math.max(rect.top, top),\n      bottom: Math.min(rect.bottom, top + height),\n      left: rect.left,\n      right: rect.right,\n    };\n  }, []);\n\n  const scrollBy = useCallback((dy: number) => {\n    const el = rootRef.current;\n    if (!el) return;\n    const scrollerNow = scrollerRef.current;\n    if (!scrollerNow) el.scrollTop += dy;\n    else if (isWindowScroller(scrollerNow)) scrollerNow.scrollBy(0, dy);\n    else scrollerNow.scrollTop += dy;\n  }, []);\n\n  useLayoutEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    // Measure synchronously before the first paint so the initial render fills\n    // the viewport, instead of painting a few overscan rows and re-rendering.\n    syncViewport();\n    const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(() => syncViewport());\n    // Track width too: `fit` columns are measured against this box and capped at\n    // its width, so when the container settles to its real width after a route\n    // transition / async layout, that change has to re-trigger the measure —\n    // otherwise an early measure against a 0/tiny width gets pinned and sticks.\n    observer?.observe(el);\n    if (!scroller) return () => observer?.disconnect();\n    // Where the list sits inside its scrollport is half of every measurement,\n    // and an element that merely *moves* fires nothing. The scrollport's own\n    // content box catches the common cause, something above the list growing;\n    // `remeasure()` is there for the rest.\n    observer?.observe(isWindowScroller(scroller) ? document.documentElement : scroller);\n    window.addEventListener('resize', syncViewport);\n    return () => {\n      observer?.disconnect();\n      window.removeEventListener('resize', syncViewport);\n    };\n  }, [scroller, syncViewport]);\n\n  // Read through a ref so the trigger below stays stable even when the consumer\n  // passes a fresh `onLoadMore` closure on every render.\n  const onLoadMoreRef = useRef(onLoadMore);\n  useLayoutEffect(() => {\n    onLoadMoreRef.current = onLoadMore;\n  });\n\n  /** Start a load unless one is already in flight (and drive the bottom spinner). */\n  const triggerLoadMore = useCallback(() => {\n    const load = onLoadMoreRef.current;\n    if (!load || isLoadingMoreRef.current) return;\n    isLoadingMoreRef.current = true;\n    setShowLoadMoreSpinner(true);\n    load().finally(() => {\n      isLoadingMoreRef.current = false;\n      setShowLoadMoreSpinner(false);\n    });\n  }, []);\n\n  // One ask per batch, the same probe the fill check below uses. The end of the\n  // list staying in view keeps qualifying, and in `scrollParent` mode it does so\n  // for the whole rest of the page.\n  const loadMoreProbeRef = useRef(-1);\n  const handleScroll = useCallback(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    // Only the load-more check needs its answer now; the render measures again in\n    // the frame, so a scroll-anchoring correction landing in between is not undone\n    // by a reading taken before it.\n    const v = measureViewport(el, scrollerRef.current);\n    // Coalesce scroll-driven re-renders to at most one per animation frame.\n    // Scroll events can fire several times per frame (especially while dragging\n    // the scrollbar thumb); rendering the visible window once per frame keeps\n    // the content in step with the cursor instead of lagging behind.\n    if (scrollRafRef.current == null) {\n      scrollRafRef.current = requestAnimationFrame(() => {\n        scrollRafRef.current = null;\n        syncViewport();\n      });\n    }\n\n    if (v.distanceToEnd <= loadMoreThreshold && loadMoreProbeRef.current !== keysRef.current.length) {\n      loadMoreProbeRef.current = keysRef.current.length;\n      triggerLoadMore();\n    }\n  }, [syncViewport, loadMoreThreshold, triggerLoadMore]);\n\n  // Imperative rather than an `onScroll` prop, because with `scrollParent` the\n  // events happen on an element the list does not render. Doing it the same way\n  // in both modes also leaves the root's `onScroll` free for the consumer.\n  useEffect(() => {\n    const target: EventTarget | null = scroller ?? rootRef.current;\n    if (!target) return;\n    target.addEventListener('scroll', handleScroll, { passive: true });\n    return () => target.removeEventListener('scroll', handleScroll);\n  }, [scroller, handleScroll]);\n\n  useEffect(\n    () => () => {\n      if (scrollRafRef.current != null) cancelAnimationFrame(scrollRafRef.current);\n    },\n    [],\n  );\n\n  useLayoutEffect(() => {\n    keysRef.current = keys;\n  });\n\n  // Resolve the gap to px before paint, re-running when the value or the\n  // container's layout changes (a theme/font change that alters em/var is caught\n  // by the container ResizeObserver feeding containerWidth/Height).\n  useLayoutEffect(() => {\n    const px = resolveCssLength(rowGap, rootRef.current);\n    setResolvedRowGap((prev) => (prev === px ? prev : px));\n  }, [rowGap, containerWidth, containerHeight]);\n\n  // What an unmeasured row is assumed to be. `estimatedRowHeight` is only the\n  // seed: once rows have been measured, their average is a far better guess, and\n  // it is the guess every row between here and a distant jump target is made of.\n  // With a fixed estimate the error compounds over that distance (40 assumed\n  // against 35 real puts row 75 at row 86), and no amount of re-aiming fixes it,\n  // because the rows that are wrong are precisely the ones never rendered.\n  const assumedRowHeight = useMemo(() => {\n    if (heightsMap.size === 0) return estimatedRowHeight;\n    let total = 0;\n    for (const height of heightsMap.values()) total += height;\n    return total / heightsMap.size;\n  }, [heightsMap, estimatedRowHeight]);\n\n  const { offsets, totalHeight } = useMemo(\n    () => buildOffsets(keys, heightsMap, assumedRowHeight, resolvedRowGap),\n    [keys, heightsMap, assumedRowHeight, resolvedRowGap],\n  );\n\n  // The scroll trigger above can only fire on a list that actually scrolls. When\n  // a batch is smaller than the viewport there is no overflow, so no scroll event\n  // ever happens and lazy loading dead-ends after the first batch. Keep asking\n  // for more until the content overflows; the consumer ends the loop by appending\n  // nothing (or by dropping `onLoadMore` once its source is exhausted).\n  //\n  // Each (row count, viewport height) pair is probed at most once, so rows\n  // settling to their measured heights can't re-ask a source that already ran\n  // dry, and progress depends entirely on the consumer appending rows.\n  //\n  // The bar to clear is the whole scrollport, not the slice the list occupies\n  // now: a windowed list scrolling in from the bottom of a page would otherwise\n  // stop filling at whatever happened to show.\n  const fillProbeRef = useRef('');\n  const fillHeight = isWindowed ? viewportHeight : containerHeight;\n  useEffect(() => {\n    if (!onLoadMore || isLoading || showLoadMoreSpinner) return;\n    // 0 while the list is unmeasured or `display: none` — nothing to fill yet.\n    if (fillHeight === 0) return;\n    // The sticky header eats into the scroll area, so it isn't usable space.\n    const headerH = headerRef.current?.offsetHeight ?? 0;\n    if (totalHeight >= fillHeight - headerH) return; // already scrollable\n    const probe = `${items.length}:${fillHeight}`;\n    if (fillProbeRef.current === probe) return;\n    fillProbeRef.current = probe;\n    triggerLoadMore();\n  }, [onLoadMore, isLoading, showLoadMoreSpinner, items.length, totalHeight, fillHeight, triggerLoadMore]);\n\n  /** Put list-space offset `top` at the top of the scrollport, whoever owns it. */\n  const scrollToOffset = useCallback((top: number, behavior?: ScrollBehavior) => {\n    const el = rootRef.current;\n    if (!el) return;\n    const scrollerNow = scrollerRef.current;\n    if (!scrollerNow) {\n      const clamped = Math.max(0, top);\n      // Assignment rather than `scrollTo` when no behaviour was asked for: it is\n      // what the anchoring correction needs, and the two are the same for `auto`.\n      if (behavior && typeof el.scrollTo === 'function') el.scrollTo({ top: clamped, behavior });\n      else el.scrollTop = clamped;\n      return;\n    }\n    // List space is relative, so the move is a delta on where the scrollport is\n    // now, and `rel` is unclamped here: a list still below it has to be reachable\n    // from above as well as from within.\n    const { rel } = measureViewport(el, scrollerNow);\n    if (isWindowScroller(scrollerNow)) {\n      scrollerNow.scrollTo({ top: Math.max(0, scrollerNow.scrollY + top - rel), behavior: behavior ?? 'auto' });\n      return;\n    }\n    const target = Math.max(0, scrollerNow.scrollTop + top - rel);\n    if (behavior && typeof scrollerNow.scrollTo === 'function') scrollerNow.scrollTo({ top: target, behavior });\n    else scrollerNow.scrollTop = target;\n  }, []);\n\n  useLayoutEffect(() => {\n    const el = rootRef.current;\n    const prevOffsets = prevOffsetsRef.current;\n    prevOffsetsRef.current = offsets;\n\n    // Nothing to anchor against on the first pass or when pinned at the top.\n    if (!el || prevOffsets.length === 0) return;\n    // A jump in flight is aiming at a row; anchoring would hold the row it\n    // happens to be passing instead, and the two would fight to a standstill.\n    if (pendingJumpRef.current) return;\n    const curScrollTop = getVisibleSlice().top;\n    if (curScrollTop <= 0) return;\n\n    // Scroll anchoring: pin the row at the top of the viewport. Find it under\n    // the *previous* offsets, then re-derive scrollTop from its *new* offset so\n    // it stays visually fixed when rows above it are measured or resized. This\n    // is more robust than summing per-row height deltas — it can't drift, and\n    // one correction covers any number of simultaneous changes above the fold.\n    const prevCount = prevOffsets.length - 1;\n    const anchor = findStartIndex(prevOffsets, curScrollTop, prevCount);\n    if (anchor >= items.length) return;\n    const within = curScrollTop - prevOffsets[anchor];\n    const target = offsets[anchor] + within;\n    if (Math.abs(target - curScrollTop) > 0.5) {\n      scrollToOffset(target);\n      setScrollTop(target);\n    }\n  }, [offsets, items.length, getVisibleSlice, scrollToOffset]);\n\n  /** Scroll position that puts row `index` where `align` asks for, or null if there is none. */\n  const computeScrollTop = useCallback(\n    (index: number, options?: VirtualListScrollOptions): number | null => {\n      const el = rootRef.current;\n      if (!el || items.length === 0) return null;\n      const i = Math.max(0, Math.min(index, items.length - 1));\n      const rowTop = offsets[i] ?? 0;\n      const rowHeight = (offsets[i + 1] ?? rowTop) - rowTop;\n      // Row offsets are measured from the top of the body, which itself starts\n      // below the sticky header: the header is in flow, so scrolling by exactly\n      // `rowTop` already puts the row flush under it. Subtracting the header\n      // height on top of that, as this once did, left the row one header lower,\n      // which in a table whose rows are about as tall as its header reads as\n      // landing a row short.\n      const headerHeight = headerRef.current?.offsetHeight ?? 0;\n      // The whole scrollport, not the slice shown now: the jump is aimed at where\n      // the row lands after the scroll, by which point the list covers it.\n      const usable = Math.max(0, measureViewport(el, scrollerRef.current).viewportHeight - headerHeight);\n      const align = options?.align ?? 'start';\n      let top = rowTop;\n      if (align === 'center') top -= (usable - rowHeight) / 2;\n      else if (align === 'end') top -= usable - rowHeight;\n      return top;\n    },\n    [items.length, offsets],\n  );\n\n  // A jump lands on the wrong row while the rows between here and there have\n  // never been rendered: their offsets are `estimatedRowHeight` guesses, and the\n  // error compounds over the distance (40px guessed against 35px real puts row\n  // 75 at row 86). Scrolling there renders those rows, measuring corrects the\n  // offsets, and the effect below re-aims at the target until it stops moving.\n  // This is why the same jump is accurate once the list has been scrolled\n  // through: by then every row in between carries a measured height.\n  const pendingJumpRef = useRef<{\n    index: number;\n    options?: VirtualListScrollOptions;\n    lastTarget: number;\n    attempts: number;\n    items: T[];\n  } | null>(null);\n\n  // Deliberately without a dependency list: every commit is a chance that new\n  // measurements have moved the target, including the ones that come from the\n  // scroll position itself changing mid-animation.\n  useLayoutEffect(() => {\n    const pending = pendingJumpRef.current;\n    const el = rootRef.current;\n    if (!pending) return;\n    const target = pending.items === items && el ? computeScrollTop(pending.index, pending.options) : null;\n    if (target == null) {\n      pendingJumpRef.current = null; // new data supersedes an in-flight jump\n      return;\n    }\n    const isStable = Math.abs(target - pending.lastTarget) <= 1;\n    // Only once it has both stopped moving and been reached: with a smooth\n    // behaviour the target can sit still for several frames while the animation\n    // is still on its way there, and giving up then would leave it short.\n    if ((isStable && Math.abs(getVisibleSlice().top - target) <= 1) || pending.attempts >= MAX_JUMP_CORRECTIONS) {\n      pendingJumpRef.current = null;\n      return;\n    }\n    if (isStable) return;\n    pending.lastTarget = target;\n    pending.attempts += 1;\n    scrollToOffset(target, pending.options?.behavior);\n  });\n\n  // Any input of the user's own outranks a jump still in flight: being pulled\n  // back to a row you just scrolled away from is worse than not arriving.\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const cancel = () => {\n      pendingJumpRef.current = null;\n    };\n    // The wheel and the finger belong to whatever scrolls; the keys belong to\n    // the list, and hijacking them page-wide would cancel on any typing.\n    const pointerTarget: EventTarget = scroller ?? el;\n    pointerTarget.addEventListener('wheel', cancel, { passive: true });\n    pointerTarget.addEventListener('touchstart', cancel, { passive: true });\n    el.addEventListener('keydown', cancel);\n    return () => {\n      pointerTarget.removeEventListener('wheel', cancel);\n      pointerTarget.removeEventListener('touchstart', cancel);\n      el.removeEventListener('keydown', cancel);\n    };\n  }, [scroller]);\n\n  const scrollApi = useMemo<Omit<VirtualListHandle, 'recalculateColumns' | 'resetColumnWidths'>>(() => {\n    function scrollToIndex(index: number, options?: VirtualListScrollOptions) {\n      const target = computeScrollTop(index, options);\n      if (target == null) return;\n      pendingJumpRef.current = { index, options, lastTarget: target, attempts: 0, items };\n      scrollToOffset(target, options?.behavior);\n    }\n    return {\n      scrollToIndex,\n      scrollToKey(key, options) {\n        const i = items.findIndex((item, idx) => (getItemKey ? getItemKey(item, idx) : idx) === key);\n        if (i >= 0) scrollToIndex(i, options);\n      },\n      scrollToTop(options) {\n        pendingJumpRef.current = null;\n        scrollToOffset(0, options?.behavior);\n      },\n      getScrollElement: () => {\n        const scrollerNow = scrollerRef.current;\n        if (!scrollerNow) return rootRef.current;\n        return isWindowScroller(scrollerNow) ? null : scrollerNow;\n      },\n      remeasure: syncViewport,\n    };\n  }, [items, getItemKey, computeScrollTop, scrollToOffset, syncViewport]);\n\n  const { startIndex, endIndex } = useMemo(\n    () => getVisibleRange(offsets, scrollTop, containerHeight, overscan, items.length),\n    [offsets, scrollTop, containerHeight, overscan, items.length],\n  );\n\n  // Virtualizing nothing fails silently: the page is simply slow, which points\n  // nowhere near the cause. That cause is almost always a viewport as tall as its\n  // content, which is what a percentage height amounts to inside a parent with no\n  // definite height of its own.\n  const warnedRef = useRef(false);\n  useEffect(() => {\n    if (warnedRef.current || containerHeight === 0) return;\n    if (items.length < NOT_VIRTUALIZING_ROWS || endIndex - startIndex + 1 < items.length) return;\n    warnedRef.current = true;\n    console.warn(\n      `VirtualList: rendering all ${items.length} rows, so nothing is being virtualized. Its viewport is as tall as its content. Give the list a definite height, or pass scrollParent to have it ride a scrollport it does not own.`,\n    );\n  }, [containerHeight, items.length, startIndex, endIndex]);\n\n  return {\n    rootRef,\n    headerRef,\n    rowObserver,\n    keysRef,\n    offsets,\n    totalHeight,\n    containerHeight,\n    viewportHeight,\n    containerWidth,\n    startIndex,\n    endIndex,\n    showLoadMoreSpinner,\n    isWindowed,\n    getVisibleSlice,\n    getVisibleRect,\n    scrollBy,\n    scrollApi,\n  };\n}\n"],"mappings":"6YAcA,SAAS,GAAiB,EAAwB,EAAuC,CACvF,GAAI,OAAO,GAAU,SAAU,OAAO,EACtC,IAAM,EAAU,EAAM,KAAK,EAC3B,GAAI,IAAY,GAAI,MAAO,GAC3B,IAAM,EAAW,OAAO,CAAO,EAC/B,GAAI,CAAC,OAAO,MAAM,CAAQ,EAAG,OAAO,EACpC,IAAM,EAAU,oBAAoB,KAAK,CAAO,EAChD,GAAI,EAAS,OAAO,WAAW,EAAQ,EAAE,EACzC,GAAI,OAAO,SAAa,KAAe,CAAC,EAAW,MAAO,GAC1D,IAAM,EAAQ,SAAS,cAAc,KAAK,EAC1C,EAAM,MAAM,QAAU,8CAA8C,EAAQ,GAC5E,EAAU,YAAY,CAAK,EAC3B,IAAM,EAAK,EAAM,sBAAsB,CAAC,CAAC,OAEzC,OADA,EAAU,YAAY,CAAK,EACpB,OAAO,SAAS,CAAE,EAAI,EAAK,CACpC,CAGA,IAAM,GAAwB,IAsExB,GAAuB,GAE7B,SAAgB,EAAoB,CAClC,QACA,OACA,aACA,sBACA,SACA,YACA,aACA,oBACA,YACA,gBACkD,CAClD,GAAM,CAAC,EAAW,IAAgB,EAAS,CAAC,EACtC,CAAC,EAAiB,IAAsB,EAAS,CAAC,EAClD,CAAC,GAAgB,IAAqB,EAAS,CAAC,EAChD,CAAC,EAAgB,IAAqB,EAAS,CAAC,EAIhD,CAAC,EAAgB,IAAqB,MAAuB,GAAiB,EAAQ,IAAI,CAAC,EAC3F,CAAC,EAAY,IAAiB,EAAuC,IAAI,GAAK,EAC9E,CAAC,EAAqB,GAA0B,EAAS,EAAK,EAC9D,EAAmB,EAAO,EAAK,EAE/B,EAAU,EAAuB,IAAI,EACrC,EAAY,EAAuB,IAAI,EACvC,EAAiB,EAAiB,CAAC,CAAC,EACpC,EAAe,EAAsB,IAAI,EACzC,EAAU,EAA4B,CAAC,CAAC,EAExC,EAAa,GAAgB,KAG7B,EAAc,EAAwB,IAAI,EAC1C,EAAa,EAAwB,IAAI,EACzC,CAAC,EAAU,IAAe,EAA0B,IAAI,EAM9D,MAAsB,CACpB,IAAI,EACJ,GAAI,IAAiB,UAAW,CAG9B,IAAM,EAAS,EAAW,SACtB,CAAC,GAAW,CAAC,EAAiB,CAAM,GAAK,CAAC,EAAO,eACnD,EAAW,QAAU,EAAQ,QAAW,EAAgB,EAAQ,OAAO,GAAK,OAAU,MACxF,EAAO,EAAW,OACpB,KACE,GAAO,EAAgB,CAAY,EAErC,EAAY,QAAU,EACtB,GAAa,GAAU,IAAS,EAAO,EAAO,CAAK,CACrD,CAAC,EAOD,GAAM,CAAC,GAAe,MAChB,OAAO,eAAmB,IAAoB,KAC3C,IAAI,eAAgB,GAAY,CACrC,GAAe,GAAS,CACtB,IAAI,EAA4C,KAChD,IAAK,IAAM,KAAS,EAAS,CAC3B,IAAM,EAAK,EAAM,OACX,EAAM,OAAO,EAAG,QAAQ,KAAK,EACnC,GAAI,OAAO,MAAM,CAAG,EAAG,SAEvB,IAAM,EAAM,EAAQ,QAAQ,GAC5B,GAAI,IAAQ,IAAA,GAAW,SAGvB,IAAM,EAAM,EAAM,gBAAgB,GAC5B,EAAI,EAAM,KAAK,MAAM,EAAI,SAAS,EAAI,EAAG,aAC3C,EAAK,IAAI,CAAG,IAAM,IACtB,AAAW,IAAO,IAAI,IAAI,CAAI,EAC9B,EAAK,IAAI,EAAK,CAAC,EACjB,CACA,OAAO,GAAQ,CACjB,CAAC,CACH,CAAC,CACF,EACD,UAAsB,GAAa,WAAW,EAAG,CAAC,CAAW,CAAC,EAE9D,IAAM,EAAgB,EAAa,GAAgB,CACjD,IAAM,EAAM,KAAK,IAAI,EAAG,EAAE,GAAG,EAC7B,GAAc,GAAU,IAAS,EAAM,EAAO,CAAI,EAClD,GAAoB,GAAU,IAAS,EAAE,cAAgB,EAAO,EAAE,aAAc,EAChF,GAAmB,GAAU,IAAS,EAAE,eAAiB,EAAO,EAAE,cAAe,EACjF,GAAmB,GAAU,IAAS,EAAE,MAAQ,EAAO,EAAE,KAAM,CACjE,EAAG,CAAC,CAAC,EAEC,EAAe,MAAkB,CACrC,IAAM,EAAK,EAAQ,QACd,GACL,EAAc,EAAgB,EAAI,EAAY,OAAO,CAAC,CACxD,EAAG,CAAC,CAAa,CAAC,EAEZ,EAAkB,MAAgC,CACtD,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,MAAO,CAAE,IAAK,EAAG,OAAQ,CAAE,EACpC,IAAM,EAAI,EAAgB,EAAI,EAAY,OAAO,EACjD,MAAO,CAAE,IAAK,KAAK,IAAI,EAAG,EAAE,GAAG,EAAG,OAAQ,EAAE,aAAc,CAC5D,EAAG,CAAC,CAAC,EAEC,GAAiB,MAA2C,CAChE,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,OAAO,KAChB,IAAM,EAAO,EAAG,sBAAsB,EAChC,EAAc,EAAY,QAChC,GAAI,CAAC,EAAa,MAAO,CAAE,IAAK,EAAK,IAAK,OAAQ,EAAK,OAAQ,KAAM,EAAK,KAAM,MAAO,EAAK,KAAM,EAClG,GAAM,CAAE,MAAK,UAAW,EAAiB,CAAW,EACpD,MAAO,CACL,IAAK,KAAK,IAAI,EAAK,IAAK,CAAG,EAC3B,OAAQ,KAAK,IAAI,EAAK,OAAQ,EAAM,CAAM,EAC1C,KAAM,EAAK,KACX,MAAO,EAAK,KACd,CACF,EAAG,CAAC,CAAC,EAEC,GAAW,EAAa,GAAe,CAC3C,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,OACT,IAAM,EAAc,EAAY,QAC3B,EACI,EAAiB,CAAW,EAAG,EAAY,SAAS,EAAG,CAAE,EAC7D,EAAY,WAAa,EAFZ,EAAG,WAAa,CAGpC,EAAG,CAAC,CAAC,EAEL,MAAsB,CACpB,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,OAGT,EAAa,EACb,IAAM,EAAW,OAAO,eAAmB,IAAc,KAAO,IAAI,mBAAqB,EAAa,CAAC,EAavG,OARA,GAAU,QAAQ,CAAE,EACf,GAKL,GAAU,QAAQ,EAAiB,CAAQ,EAAI,SAAS,gBAAkB,CAAQ,EAClF,OAAO,iBAAiB,SAAU,CAAY,MACjC,CACX,GAAU,WAAW,EACrB,OAAO,oBAAoB,SAAU,CAAY,CACnD,OAV4B,GAAU,WAAW,CAWnD,EAAG,CAAC,EAAU,CAAY,CAAC,EAI3B,IAAM,GAAgB,EAAO,CAAU,EACvC,MAAsB,CACpB,GAAc,QAAU,CAC1B,CAAC,EAGD,IAAM,EAAkB,MAAkB,CACxC,IAAM,EAAO,GAAc,QACtB,GAAQ,GAAiB,UAC9B,EAAiB,QAAU,GAC3B,EAAuB,EAAI,EAC3B,EAAK,CAAC,CAAC,YAAc,CACnB,EAAiB,QAAU,GAC3B,EAAuB,EAAK,CAC9B,CAAC,EACH,EAAG,CAAC,CAAC,EAKC,GAAmB,EAAO,EAAE,EAC5B,EAAe,MAAkB,CACrC,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,OAIT,IAAM,EAAI,EAAgB,EAAI,EAAY,OAAO,EAKjD,AACE,EAAa,UAAU,0BAA4B,CACjD,EAAa,QAAU,KACvB,EAAa,CACf,CAAC,EAGC,EAAE,eAAiB,GAAqB,GAAiB,UAAY,EAAQ,QAAQ,SACvF,GAAiB,QAAU,EAAQ,QAAQ,OAC3C,EAAgB,EAEpB,EAAG,CAAC,EAAc,EAAmB,CAAe,CAAC,EAKrD,MAAgB,CACd,IAAM,EAA6B,GAAY,EAAQ,QAClD,KAEL,OADA,EAAO,iBAAiB,SAAU,EAAc,CAAE,QAAS,EAAK,CAAC,MACpD,EAAO,oBAAoB,SAAU,CAAY,CAChE,EAAG,CAAC,EAAU,CAAY,CAAC,EAE3B,UACc,CACN,EAAa,SAAW,MAAM,qBAAqB,EAAa,OAAO,CAC7E,EACA,CAAC,CACH,EAEA,MAAsB,CACpB,EAAQ,QAAU,CACpB,CAAC,EAKD,MAAsB,CACpB,IAAM,EAAK,GAAiB,EAAQ,EAAQ,OAAO,EACnD,GAAmB,GAAU,IAAS,EAAK,EAAO,CAAG,CACvD,EAAG,CAAC,EAAQ,EAAgB,CAAe,CAAC,EAQ5C,IAAM,GAAmB,MAAc,CACrC,GAAI,EAAW,OAAS,EAAG,OAAO,GAClC,IAAI,EAAQ,EACZ,IAAK,IAAM,KAAU,EAAW,OAAO,EAAG,GAAS,EACnD,OAAO,EAAQ,EAAW,IAC5B,EAAG,CAAC,EAAY,EAAkB,CAAC,EAE7B,CAAE,UAAS,eAAgB,MACzB,EAAa,EAAM,EAAY,GAAkB,CAAc,EACrE,CAAC,EAAM,EAAY,GAAkB,CAAc,CACrD,EAeM,GAAe,EAAO,EAAE,EACxB,EAAa,EAAa,GAAiB,EACjD,MAAgB,CAGd,GAFI,CAAC,GAAc,GAAa,GAE5B,IAAe,EAAG,OAEtB,IAAM,EAAU,EAAU,SAAS,cAAgB,EACnD,GAAI,GAAe,EAAa,EAAS,OACzC,IAAM,EAAQ,GAAG,EAAM,OAAO,GAAG,IAC7B,GAAa,UAAY,IAC7B,GAAa,QAAU,EACvB,EAAgB,EAClB,EAAG,CAAC,EAAY,EAAW,EAAqB,EAAM,OAAQ,EAAa,EAAY,CAAe,CAAC,EAGvG,IAAM,EAAiB,GAAa,EAAa,IAA8B,CAC7E,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,OACT,IAAM,EAAc,EAAY,QAChC,GAAI,CAAC,EAAa,CAChB,IAAM,EAAU,KAAK,IAAI,EAAG,CAAG,EAG3B,GAAY,OAAO,EAAG,UAAa,WAAY,EAAG,SAAS,CAAE,IAAK,EAAS,UAAS,CAAC,EACpF,EAAG,UAAY,EACpB,MACF,CAIA,GAAM,CAAE,OAAQ,EAAgB,EAAI,CAAW,EAC/C,GAAI,EAAiB,CAAW,EAAG,CACjC,EAAY,SAAS,CAAE,IAAK,KAAK,IAAI,EAAG,EAAY,QAAU,EAAM,CAAG,EAAG,SAAU,GAAY,MAAO,CAAC,EACxG,MACF,CACA,IAAM,EAAS,KAAK,IAAI,EAAG,EAAY,UAAY,EAAM,CAAG,EACxD,GAAY,OAAO,EAAY,UAAa,WAAY,EAAY,SAAS,CAAE,IAAK,EAAQ,UAAS,CAAC,EACrG,EAAY,UAAY,CAC/B,EAAG,CAAC,CAAC,EAEL,MAAsB,CACpB,IAAM,EAAK,EAAQ,QACb,EAAc,EAAe,QAOnC,GANA,EAAe,QAAU,EAGrB,CAAC,GAAM,EAAY,SAAW,GAG9B,EAAe,QAAS,OAC5B,IAAM,EAAe,EAAgB,CAAC,CAAC,IACvC,GAAI,GAAgB,EAAG,OAOvB,IAAM,EAAY,EAAY,OAAS,EACjC,EAAS,EAAe,EAAa,EAAc,CAAS,EAClE,GAAI,GAAU,EAAM,OAAQ,OAC5B,IAAM,EAAS,EAAe,EAAY,GACpC,EAAS,EAAQ,GAAU,EAC7B,KAAK,IAAI,EAAS,CAAY,EAAI,KACpC,EAAe,CAAM,EACrB,GAAa,CAAM,EAEvB,EAAG,CAAC,EAAS,EAAM,OAAQ,EAAiB,CAAc,CAAC,EAG3D,IAAM,EAAmB,GACtB,EAAe,IAAsD,CACpE,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,GAAM,EAAM,SAAW,EAAG,OAAO,KACtC,IAAM,EAAI,KAAK,IAAI,EAAG,KAAK,IAAI,EAAO,EAAM,OAAS,CAAC,CAAC,EACjD,EAAS,EAAQ,IAAM,EACvB,GAAa,EAAQ,EAAI,IAAM,GAAU,EAOzC,EAAe,EAAU,SAAS,cAAgB,EAGlD,EAAS,KAAK,IAAI,EAAG,EAAgB,EAAI,EAAY,OAAO,CAAC,CAAC,eAAiB,CAAY,EAC3F,EAAQ,GAAS,OAAS,QAC5B,EAAM,EAGV,OAFI,IAAU,SAAU,IAAQ,EAAS,GAAa,EAC7C,IAAU,QAAO,GAAO,EAAS,GACnC,CACT,EACA,CAAC,EAAM,OAAQ,CAAO,CACxB,EASM,EAAiB,EAMb,IAAI,EAKd,MAAsB,CACpB,IAAM,EAAU,EAAe,QACzB,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAS,OACd,IAAM,EAAS,EAAQ,QAAU,GAAS,EAAK,EAAiB,EAAQ,MAAO,EAAQ,OAAO,EAAI,KAClG,GAAI,GAAU,KAAM,CAClB,EAAe,QAAU,KACzB,MACF,CACA,IAAM,EAAW,KAAK,IAAI,EAAS,EAAQ,UAAU,GAAK,EAI1D,GAAK,GAAY,KAAK,IAAI,EAAgB,CAAC,CAAC,IAAM,CAAM,GAAK,GAAM,EAAQ,UAAY,GAAsB,CAC3G,EAAe,QAAU,KACzB,MACF,CACI,IACJ,EAAQ,WAAa,EACrB,EAAQ,UAAY,EACpB,EAAe,EAAQ,EAAQ,SAAS,QAAQ,EAClD,CAAC,EAID,MAAgB,CACd,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,OACT,IAAM,MAAe,CACnB,EAAe,QAAU,IAC3B,EAGM,EAA6B,GAAY,EAI/C,OAHA,EAAc,iBAAiB,QAAS,EAAQ,CAAE,QAAS,EAAK,CAAC,EACjE,EAAc,iBAAiB,aAAc,EAAQ,CAAE,QAAS,EAAK,CAAC,EACtE,EAAG,iBAAiB,UAAW,CAAM,MACxB,CACX,EAAc,oBAAoB,QAAS,CAAM,EACjD,EAAc,oBAAoB,aAAc,CAAM,EACtD,EAAG,oBAAoB,UAAW,CAAM,CAC1C,CACF,EAAG,CAAC,CAAQ,CAAC,EAEb,IAAM,GAAY,MAAmF,CACnG,SAAS,EAAc,EAAe,EAAoC,CACxE,IAAM,EAAS,EAAiB,EAAO,CAAO,EAC1C,GAAU,OACd,EAAe,QAAU,CAAE,QAAO,UAAS,WAAY,EAAQ,SAAU,EAAG,OAAM,EAClF,EAAe,EAAQ,GAAS,QAAQ,EAC1C,CACA,MAAO,CACL,gBACA,YAAY,EAAK,EAAS,CACxB,IAAM,EAAI,EAAM,WAAW,EAAM,KAAS,EAAa,EAAW,EAAM,CAAG,EAAI,KAAS,CAAG,EACvF,GAAK,GAAG,EAAc,EAAG,CAAO,CACtC,EACA,YAAY,EAAS,CACnB,EAAe,QAAU,KACzB,EAAe,EAAG,GAAS,QAAQ,CACrC,EACA,qBAAwB,CACtB,IAAM,EAAc,EAAY,QAEhC,OADK,EACE,EAAiB,CAAW,EAAI,KAAO,EADrB,EAAQ,OAEnC,EACA,UAAW,CACb,CACF,EAAG,CAAC,EAAO,EAAY,EAAkB,EAAgB,CAAY,CAAC,EAEhE,CAAE,aAAY,YAAa,MACzB,EAAgB,EAAS,EAAW,EAAiB,GAAU,EAAM,MAAM,EACjF,CAAC,EAAS,EAAW,EAAiB,GAAU,EAAM,MAAM,CAC9D,EAMM,EAAY,EAAO,EAAK,EAU9B,OATA,MAAgB,CACV,EAAU,SAAW,IAAoB,GACzC,EAAM,OAAS,IAAyB,EAAW,EAAa,EAAI,EAAM,SAC9E,EAAU,QAAU,GACpB,QAAQ,KACN,8BAA8B,EAAM,OAAO,oLAC7C,EACF,EAAG,CAAC,EAAiB,EAAM,OAAQ,EAAY,CAAQ,CAAC,EAEjD,CACL,UACA,YACA,cACA,UACA,UACA,cACA,kBACA,kBACA,iBACA,aACA,WACA,sBACA,aACA,kBACA,kBACA,YACA,YACF,CACF"}