{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "virtual-list",
  "title": "Virtual List",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/virtual-list/VirtualList.vue",
      "content": "<script setup lang=\"ts\" generic=\"T extends Record<string, any>\">\nimport { computed, onMounted, ref, watch } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(\n  defineProps<{\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    class?: HTMLAttributes['class']\n  }>(),\n  {\n    overscan: 3,\n    keyField: 'id',\n    direction: 'vertical',\n    as: 'div',\n  },\n)\n\nconst emits = defineEmits<{\n  (e: 'scroll', event: Event): void\n  (e: 'range-change', range: [number, number]): void\n}>()\n\nconst scrollEl = ref<HTMLElement | null>(null)\nconst scrollOffset = ref(0)\nconst viewportSize = ref(0)\n\nconst isVertical = computed(() => props.direction === 'vertical')\n\nconst offsets = computed<number[]>(() => {\n  const out: number[] = [0]\n  for (let i = 0; i < props.items.length; i++) {\n    const s = typeof props.itemSize === 'function' ? props.itemSize(props.items[i]!, i) : props.itemSize\n    out.push(out[i]! + s)\n  }\n  return out\n})\n\nconst totalSize = computed(() => offsets.value[props.items.length] ?? 0)\n\nfunction findIndex(target: number): number {\n  const o = offsets.value\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\nconst range = computed<[number, number]>(() => {\n  if (!props.items.length || viewportSize.value === 0) return [0, 0]\n  const start = Math.max(0, findIndex(scrollOffset.value) - props.overscan)\n  const end = Math.min(props.items.length, findIndex(scrollOffset.value + viewportSize.value) + 1 + props.overscan)\n  return [start, end]\n})\n\nconst visible = computed(() => props.items.slice(range.value[0], range.value[1]))\nconst offsetStart = computed(() => offsets.value[range.value[0]] ?? 0)\n\nwatch(range, (r) => emits('range-change', r))\n\nfunction onScroll(e: Event) {\n  const el = e.target as HTMLElement\n  scrollOffset.value = isVertical.value ? el.scrollTop : el.scrollLeft\n  emits('scroll', e)\n}\n\nfunction measure() {\n  if (!scrollEl.value) return\n  viewportSize.value = isVertical.value ? scrollEl.value.clientHeight : scrollEl.value.clientWidth\n}\n\nonMounted(() => {\n  measure()\n  if (typeof ResizeObserver !== 'undefined' && scrollEl.value) {\n    const ro = new ResizeObserver(measure)\n    ro.observe(scrollEl.value)\n  }\n})\n\nwatch(() => props.height, measure, { flush: 'post' })\n\nfunction sizeOf(item: T, i: number) {\n  return typeof props.itemSize === 'function' ? props.itemSize(item, i) : props.itemSize\n}\n\nfunction getKey(item: T, fallback: number): string | number {\n  const k = item[props.keyField]\n  return k ?? fallback\n}\n\nfunction scrollToOffset(px: number) {\n  if (!scrollEl.value) return\n  if (isVertical.value) scrollEl.value.scrollTop = px\n  else scrollEl.value.scrollLeft = px\n}\n\nfunction scrollToIndex(i: number, options: { align?: 'start' | 'center' | 'end' } = {}) {\n  const align = options.align ?? 'start'\n  const start = offsets.value[i] ?? 0\n  const size = (offsets.value[i + 1] ?? start) - start\n  let target = start\n  if (align === 'center') target = start - viewportSize.value / 2 + size / 2\n  else if (align === 'end') target = start - viewportSize.value + size\n  scrollToOffset(Math.max(0, target))\n}\n\nfunction getVisibleRange(): [number, number] {\n  return range.value\n}\n\ndefineExpose({ scrollToOffset, scrollToIndex, getVisibleRange })\n\nconst containerStyle = computed((): Record<string, string> => {\n  const h = typeof props.height === 'number' ? `${props.height}px` : props.height\n  return isVertical.value ? { height: h, overflowY: 'auto' } : { width: h, overflowX: 'auto' }\n})\n\nconst innerStyle = computed(\n  (): Record<string, string> =>\n    isVertical.value\n      ? { height: `${totalSize.value}px`, position: 'relative', width: '100%' }\n      : { width: `${totalSize.value}px`, position: 'relative', height: '100%' },\n)\n\nconst offsetStyle = computed(\n  (): Record<string, string> =>\n    isVertical.value\n      ? { transform: `translateY(${offsetStart.value}px)` }\n      : { transform: `translateX(${offsetStart.value}px)`, height: '100%', display: 'flex' },\n)\n</script>\n\n<template>\n  <component\n    :is=\"as\"\n    ref=\"scrollEl\"\n    data-uipkge\n    data-slot=\"virtual-list\"\n    :class=\"cn('w-full', props.class)\"\n    :style=\"containerStyle\"\n    @scroll.passive=\"onScroll\"\n  >\n    <div :style=\"innerStyle\">\n      <div :style=\"offsetStyle\">\n        <div\n          v-for=\"(item, i) in visible\"\n          :key=\"getKey(item, range[0] + i)\"\n          :style=\"\n            isVertical\n              ? { height: sizeOf(item, range[0] + i) + 'px' }\n              : { width: sizeOf(item, range[0] + i) + 'px', flexShrink: 0 }\n          \"\n        >\n          <slot :item=\"item\" :index=\"range[0] + i\" />\n        </div>\n      </div>\n    </div>\n  </component>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/virtual-list/VirtualList.vue"
    },
    {
      "path": "packages/registry-vue/components/virtual-list/index.ts",
      "content": "export { default as VirtualList } from './VirtualList.vue'\n",
      "type": "registry:ui",
      "target": "~/app/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"
  ]
}