{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "virtual-list",
  "title": "Virtual List",
  "description": "Renders only the visible rows of a long list, with variable row heights and keyboard focus that survives scrolling.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/virtual-list.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\nimport {\n  nextRowIndex,\n  rowOffsets,\n  scrollTopToReveal,\n  visibleRange,\n} from \"@/registry/default/lib/virtual-list\"\n\nfunction indexKey(_: unknown, index: number): React.Key {\n  return index\n}\n\nfunction anchorPosition(container: HTMLElement, offsets: number[]) {\n  const { scrollTop, clientHeight } = container\n  const focused = container.querySelector<HTMLElement>(\n    '[data-slot=\"virtual-list-row\"]:focus-within'\n  )\n  if (!focused) return scrollTop\n  const index = Number(focused.dataset.index)\n  const isInView =\n    scrollTopToReveal(offsets, index, scrollTop, clientHeight) === scrollTop\n  return isInView ? offsets[index] : scrollTop\n}\n\nfunction VirtualRow({\n  rowKey,\n  onMeasure,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  rowKey: React.Key\n  onMeasure: (key: React.Key, row: HTMLElement, height: number) => void\n}) {\n  const ref = React.useRef<HTMLDivElement>(null)\n\n  React.useLayoutEffect(() => {\n    const row = ref.current\n    if (!row) return\n    const observer = new ResizeObserver(([entry]) => {\n      onMeasure(rowKey, row, entry.borderBoxSize[0].blockSize)\n    })\n    observer.observe(row)\n    return () => observer.disconnect()\n  }, [rowKey, onMeasure])\n\n  return <div ref={ref} {...props} />\n}\n\nfunction VirtualList<T>({\n  items,\n  estimateSize,\n  overscan = 4,\n  getItemKey = indexKey,\n  children,\n  ref,\n  className,\n  onScroll,\n  onKeyDown,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  items: T[]\n  estimateSize: number\n  overscan?: number\n  getItemKey?: (item: T, index: number) => React.Key\n  children: (item: T, index: number) => React.ReactNode\n}) {\n  const containerRef = React.useRef<HTMLDivElement>(null)\n  const pendingFocus = React.useRef(false)\n  const pendingReveal = React.useRef(false)\n  const pendingScrollDelta = React.useRef(0)\n  const [sizes, setSizes] = React.useState(new Map<React.Key, number>())\n  const [range, setRange] = React.useState({ first: 0, last: 0 })\n  const [activeKey, setActiveKey] = React.useState<React.Key | null>(null)\n\n  const offsets = React.useMemo(\n    () =>\n      rowOffsets(\n        items.length,\n        (index) => sizes.get(getItemKey(items[index], index)) ?? estimateSize\n      ),\n    [items, estimateSize, getItemKey, sizes]\n  )\n  const latest = React.useRef({ offsets, sizes, estimateSize })\n\n  const activeIndex =\n    activeKey === null\n      ? -1\n      : items.findIndex((item, index) => getItemKey(item, index) === activeKey)\n  const start = Math.max(0, range.first - overscan)\n  const end = Math.min(items.length - 1, range.last + overscan)\n  const rendered = Array.from(\n    { length: Math.max(0, end - start + 1) },\n    (_, index) => start + index\n  )\n  if (activeIndex !== -1 && activeIndex < start) rendered.unshift(activeIndex)\n  if (activeIndex > end) rendered.push(activeIndex)\n  const tabStop = activeIndex === -1 ? range.first : activeIndex\n\n  const syncRange = React.useCallback(() => {\n    const container = containerRef.current\n    if (!container) return\n    const next = visibleRange(\n      latest.current.offsets,\n      container.scrollTop,\n      container.clientHeight\n    )\n    setRange((previous) =>\n      previous.first === next.first && previous.last === next.last\n        ? previous\n        : next\n    )\n  }, [])\n\n  const measure = React.useCallback(\n    (key: React.Key, row: HTMLElement, height: number) => {\n      const container = containerRef.current\n      const previous =\n        latest.current.sizes.get(key) ?? latest.current.estimateSize\n      if (!container || height === 0 || height === previous) return\n      const { offsets } = latest.current\n      if (\n        offsets[Number(row.dataset.index)] < anchorPosition(container, offsets)\n      ) {\n        pendingScrollDelta.current += height - previous\n      }\n      if (row === document.activeElement) pendingReveal.current = true\n      setSizes((current) => new Map(current).set(key, height))\n    },\n    []\n  )\n\n  React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement)\n\n  React.useLayoutEffect(() => {\n    latest.current = { offsets, sizes, estimateSize }\n    const container = containerRef.current\n    if (container) {\n      container.scrollTop += pendingScrollDelta.current\n      pendingScrollDelta.current = 0\n    }\n    if (container && pendingReveal.current) {\n      pendingReveal.current = false\n      container.scrollTop = scrollTopToReveal(\n        offsets,\n        Number((document.activeElement as HTMLElement).dataset.index),\n        container.scrollTop,\n        container.clientHeight\n      )\n    }\n    syncRange()\n  }, [offsets, sizes, estimateSize, syncRange])\n\n  React.useLayoutEffect(() => {\n    const container = containerRef.current\n    if (!container) return\n    const observer = new ResizeObserver(syncRange)\n    observer.observe(container)\n    return () => observer.disconnect()\n  }, [syncRange])\n\n  React.useLayoutEffect(() => {\n    if (!pendingFocus.current) return\n    pendingFocus.current = false\n    containerRef.current\n      ?.querySelector<HTMLElement>(`[data-index=\"${activeIndex}\"]`)\n      ?.focus({ preventScroll: true })\n  }, [activeIndex])\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    onKeyDown?.(event)\n    const container = containerRef.current\n    const row = event.target as HTMLElement\n    if (event.defaultPrevented || !container) return\n    if (!row.matches('[data-slot=\"virtual-list-row\"]')) return\n    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return\n\n    const index = Number(row.dataset.index)\n    const next = nextRowIndex(offsets, index, event.key, container.clientHeight)\n    if (next === null) return\n    event.preventDefault()\n    if (next === index) return\n    container.scrollTop = scrollTopToReveal(\n      offsets,\n      next,\n      container.scrollTop,\n      container.clientHeight\n    )\n    pendingFocus.current = true\n    setActiveKey(getItemKey(items[next], next))\n  }\n\n  return (\n    <div\n      ref={containerRef}\n      role=\"list\"\n      data-slot=\"virtual-list\"\n      onScroll={(event) => {\n        onScroll?.(event)\n        syncRange()\n      }}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"relative overflow-y-auto [overflow-anchor:none]\",\n        className\n      )}\n      {...props}\n    >\n      <div\n        className=\"relative w-full\"\n        style={{ height: offsets[items.length] }}\n      >\n        {rendered.map((index) => {\n          const key = getItemKey(items[index], index)\n          return (\n            <VirtualRow\n              key={key}\n              rowKey={key}\n              onMeasure={measure}\n              role=\"listitem\"\n              data-slot=\"virtual-list-row\"\n              data-index={index}\n              aria-posinset={index + 1}\n              aria-setsize={items.length}\n              tabIndex={index === tabStop ? 0 : -1}\n              onFocus={() => setActiveKey(key)}\n              style={{ transform: `translateY(${offsets[index]}px)` }}\n              className=\"absolute top-0 left-0 w-full outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\"\n            >\n              {children(items[index], index)}\n            </VirtualRow>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\nexport { VirtualList }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/virtual-list.ts",
      "content": "export function rowOffsets(count: number, sizeOf: (index: number) => number) {\n  const offsets = new Array<number>(count + 1)\n  offsets[0] = 0\n  for (let index = 0; index < count; index++) {\n    offsets[index + 1] = offsets[index] + sizeOf(index)\n  }\n  return offsets\n}\n\nexport function rowAt(offsets: number[], position: number) {\n  let low = 0\n  let high = Math.max(0, offsets.length - 2)\n  while (low < high) {\n    const middle = Math.ceil((low + high) / 2)\n    if (offsets[middle] <= position) low = middle\n    else high = middle - 1\n  }\n  return low\n}\n\nexport function visibleRange(\n  offsets: number[],\n  scrollTop: number,\n  viewportHeight: number\n) {\n  return {\n    first: rowAt(offsets, scrollTop),\n    last: rowAt(offsets, scrollTop + viewportHeight),\n  }\n}\n\nexport function scrollTopToReveal(\n  offsets: number[],\n  index: number,\n  scrollTop: number,\n  viewportHeight: number\n) {\n  const top = offsets[index]\n  const bottom = offsets[index + 1]\n  if (top < scrollTop) return top\n  if (bottom > scrollTop + viewportHeight) return bottom - viewportHeight\n  return scrollTop\n}\n\nexport function nextRowIndex(\n  offsets: number[],\n  index: number,\n  key: string,\n  viewportHeight: number\n) {\n  const lastIndex = offsets.length - 2\n  switch (key) {\n    case \"ArrowDown\":\n      return Math.min(index + 1, lastIndex)\n    case \"ArrowUp\":\n      return Math.max(index - 1, 0)\n    case \"PageDown\":\n      return Math.max(\n        Math.min(index + 1, lastIndex),\n        rowAt(offsets, offsets[index] + viewportHeight)\n      )\n    case \"PageUp\":\n      return Math.min(\n        Math.max(index - 1, 0),\n        rowAt(offsets, offsets[index] - viewportHeight)\n      )\n    case \"Home\":\n      return 0\n    case \"End\":\n      return lastIndex\n    default:\n      return null\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}