{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "virtual-list",
  "title": "Virtual List",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/virtual-list/VirtualList.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  /** Imperative handle — grab it with `bind:this` (React `VirtualListHandle` parity). */\n  export interface VirtualListHandle {\n    scrollToOffset: (px: number) => void\n    scrollToIndex: (index: number, options?: { align?: 'start' | 'center' | 'end' }) => void\n    getVisibleRange: () => [number, number]\n  }\n\n  export interface VirtualListProps<T extends Record<string, unknown>>\n    extends Omit<HTMLAttributes<HTMLElement>, 'children'> {\n    children?: Snippet<[{ item: T; index: number }]>\n    items: T[]\n    itemSize: number | ((item: T, index: number) => number)\n    height: number | string\n    overscan?: number\n    keyField?: string\n    direction?: 'vertical' | 'horizontal'\n    as?: string\n    onscroll?: (event: Event) => void\n    onrangechange?: (range: [number, number]) => void\n  }\n</script>\n\n<script lang=\"ts\" generics=\"T extends Record<string, unknown>\">\n  import { cn } from '$lib/utils'\n\n  let {\n    children,\n    items,\n    itemSize,\n    height,\n    overscan = 3,\n    keyField = 'id',\n    direction = 'vertical',\n    as = 'div',\n    onscroll,\n    onrangechange,\n    class: className,\n    ...restProps\n  }: VirtualListProps<T> = $props()\n\n  let scrollEl: HTMLElement | null = $state(null)\n  let scrollOffset = $state(0)\n  let viewportSize = $state(typeof height === 'number' ? height : Number.parseInt(String(height), 10) || 0)\n\n  const isVertical = $derived(direction === 'vertical')\n\n  const offsets = $derived.by(() => {\n    const out: number[] = [0]\n    for (let i = 0; i < items.length; i++) {\n      const s = typeof itemSize === 'function' ? itemSize(items[i]!, i) : itemSize\n      out.push(out[i]! + s)\n    }\n    return out\n  })\n\n  const totalSize = $derived(offsets[items.length] ?? 0)\n\n  function findIndex(target: number): number {\n    const o = offsets\n    let lo = 0\n    let hi = o.length - 1\n    while (lo < hi) {\n      const mid = (lo + hi + 1) >> 1\n      if (o[mid]! <= target) lo = mid\n      else hi = mid - 1\n    }\n    return lo\n  }\n\n  const range = $derived.by((): [number, number] => {\n    if (!items.length || viewportSize === 0) return [0, 0]\n    const start = Math.max(0, findIndex(scrollOffset) - overscan)\n    const end = Math.min(items.length, findIndex(scrollOffset + viewportSize) + 1 + overscan)\n    return [start, end]\n  })\n\n  const visible = $derived(items.slice(range[0], range[1]))\n  const offsetStart = $derived(offsets[range[0]] ?? 0)\n\n  // Mirror Vue's non-immediate watcher: only fire when the window actually moves.\n  let prevRange: [number, number] = [0, 0]\n  let rangePrimed = false\n  $effect(() => {\n    const r = range\n    if (!rangePrimed) {\n      rangePrimed = true\n      prevRange = r\n      return\n    }\n    if (r[0] !== prevRange[0] || r[1] !== prevRange[1]) {\n      prevRange = r\n      onrangechange?.(r)\n    }\n  })\n\n  function onScroll(e: Event) {\n    const el = e.target as HTMLElement\n    scrollOffset = isVertical ? el.scrollTop : el.scrollLeft\n    onscroll?.(e)\n  }\n\n  function measure() {\n    if (!scrollEl) return\n    const measured = isVertical ? scrollEl.clientHeight : scrollEl.clientWidth\n    const fallback = typeof height === 'number' ? height : Number.parseInt(String(height), 10) || 0\n    viewportSize = measured || fallback\n  }\n\n  $effect(() => {\n    const el = scrollEl\n    // Track height so resizes re-measure (post-flush, like Vue's flush: 'post' watcher).\n    const _h = height\n    if (!el) return\n    queueMicrotask(measure)\n    if (typeof ResizeObserver === 'undefined') return\n    const ro = new ResizeObserver(measure)\n    ro.observe(el)\n    return () => ro.disconnect()\n  })\n\n  function sizeOf(item: T, i: number) {\n    return typeof itemSize === 'function' ? itemSize(item, i) : itemSize\n  }\n\n  function getKey(item: T, fallback: number): string | number {\n    const k = item[keyField] as string | number | undefined\n    return k ?? fallback\n  }\n\n  export function scrollToOffset(px: number) {\n    if (!scrollEl) return\n    if (isVertical) scrollEl.scrollTop = px\n    else scrollEl.scrollLeft = px\n  }\n\n  export function scrollToIndex(i: number, options: { align?: 'start' | 'center' | 'end' } = {}) {\n    const align = options.align ?? 'start'\n    const start = offsets[i] ?? 0\n    const size = (offsets[i + 1] ?? start) - start\n    let target = start\n    if (align === 'center') target = start - viewportSize / 2 + size / 2\n    else if (align === 'end') target = start - viewportSize + size\n    scrollToOffset(Math.max(0, target))\n  }\n\n  export function getVisibleRange(): [number, number] {\n    return range\n  }\n\n  const containerStyle = $derived.by((): Record<string, string> => {\n    const h = typeof height === 'number' ? `${height}px` : height\n    return isVertical ? { height: h, overflowY: 'auto' } : { width: h, overflowX: 'auto' }\n  })\n\n  const innerStyle = $derived.by((): Record<string, string> =>\n    isVertical\n      ? { height: `${totalSize}px`, position: 'relative', width: '100%' }\n      : { width: `${totalSize}px`, position: 'relative', height: '100%' },\n  )\n\n  const offsetStyle = $derived.by((): Record<string, string> =>\n    isVertical\n      ? { transform: `translateY(${offsetStart}px)` }\n      : { transform: `translateX(${offsetStart}px)`, height: '100%', display: 'flex' },\n  )\n\n  function rowStyle(item: T, index: number): string {\n    return isVertical ? `height: ${sizeOf(item, index)}px` : `width: ${sizeOf(item, index)}px; flex-shrink: 0`\n  }\n\n  function styleAttr(style: Record<string, string>): string {\n    return Object.entries(style)\n      .map(([k, v]) => `${k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}: ${v}`)\n      .join('; ')\n  }\n</script>\n\n<svelte:element\n  this={as}\n  bind:this={scrollEl}\n  data-uipkge\n  data-slot=\"virtual-list\"\n  class={cn('w-full', className)}\n  style={styleAttr(containerStyle)}\n  onscroll={onScroll}\n  {...restProps}\n>\n  <div style={styleAttr(innerStyle)}>\n    <div style={styleAttr(offsetStyle)}>\n      {#each visible as item, i (getKey(item, range[0] + i))}\n        <div style={rowStyle(item, range[0] + i)}>\n          {@render children?.({ item, index: range[0] + i })}\n        </div>\n      {/each}\n    </div>\n  </div>\n</svelte:element>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/virtual-list/VirtualList.svelte"
    },
    {
      "path": "packages/registry-svelte/components/virtual-list/index.ts",
      "content": "export { default as VirtualList, type VirtualListHandle, type VirtualListProps } from './VirtualList.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/virtual-list/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Generic windowed scroller. Renders only visible items plus a small overscan, with fixed or dynamic item sizes. Use for long lists where most rows are off-screen.",
  "categories": [
    "data-display"
  ]
}