{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "infinite-scroll",
  "title": "Infinite Scroll",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/infinite-scroll/InfiniteScroll.vue",
      "content": "<script setup lang=\"ts\" generic=\"T = any\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { Loader2 } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(\n  defineProps<{\n    /** Rendered list. The component does not mutate it; the parent appends\n     *  new items in response to the `load` event. */\n    items?: T[]\n    /** When false, the sentinel never fires `load` (end of data reached). */\n    hasMore?: boolean\n    /** True while the parent is fetching. While true the sentinel is paused\n     *  so duplicate loads are not emitted. */\n    loading?: boolean\n    /** Distance (px) from the boundary at which `load` fires. Larger = earlier. */\n    distance?: number\n    /** Scroll container. `\"window\"` listens on the viewport; pass an element\n     *  ref (HTMLElement) or a CSS selector string to listen on a scrollable\n     *  element instead. */\n    scrollTarget?: 'window' | HTMLElement | string\n    /** Reverse mode: prepend items at the top. The sentinel is anchored to the\n     *  top edge and `load` fires when the user scrolls near the top. */\n    reverse?: boolean\n    /** Hard pause independent of `loading`/`hasMore`. */\n    disabled?: boolean\n    /** Hide the default loading spinner (use the `loading` slot instead). */\n    hideSpinner?: boolean\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    items: () => [] as T[],\n    hasMore: true,\n    loading: false,\n    distance: 0,\n    scrollTarget: 'window',\n    reverse: false,\n    disabled: false,\n    hideSpinner: false,\n  },\n)\n\nconst emit = defineEmits<{\n  (e: 'load'): void\n}>()\n\nconst sentinel = ref<HTMLElement | null>(null)\nlet scrollEl: HTMLElement | Window | null = null\n\nconst showSpinner = computed(() => props.loading && !props.hideSpinner)\n\nfunction getScrollElement(): HTMLElement | Window | null {\n  if (props.scrollTarget === 'window') return window\n  if (typeof props.scrollTarget === 'string') {\n    return (document.querySelector(props.scrollTarget) as HTMLElement | null) ?? window\n  }\n  return props.scrollTarget\n}\n\nfunction check() {\n  if (props.disabled || props.loading || !props.hasMore || !sentinel.value) return\n  const sentinelRect = sentinel.value.getBoundingClientRect()\n  const viewportH = window.innerHeight || document.documentElement.clientHeight\n  const viewportW = window.innerWidth || document.documentElement.clientWidth\n  if (props.reverse) {\n    // Reverse: fire when the sentinel (anchored at top) approaches the top edge.\n    if (sentinelRect.bottom >= -props.distance && sentinelRect.top <= viewportH) {\n      emit('load')\n    }\n  } else {\n    // Forward: fire when the sentinel approaches the bottom edge.\n    if (sentinelRect.top <= viewportH + props.distance && sentinelRect.bottom >= -props.distance) {\n      void viewportW\n      emit('load')\n    }\n  }\n}\n\nfunction onScroll() {\n  check()\n}\n\nonMounted(() => {\n  scrollEl = getScrollElement()\n  scrollEl?.addEventListener('scroll', onScroll, { passive: true })\n  // Fire once on mount so an initially-empty list starts loading immediately.\n  check()\n})\n\nonBeforeUnmount(() => {\n  scrollEl?.removeEventListener('scroll', onScroll)\n})\n\n// Re-bind the listener when the scroll target changes.\nwatch(\n  () => props.scrollTarget,\n  () => {\n    scrollEl?.removeEventListener('scroll', onScroll)\n    scrollEl = getScrollElement()\n    scrollEl?.addEventListener('scroll', onScroll, { passive: true })\n    check()\n  },\n)\n\n// When a load completes (loading flips false) and there is still more data,\n// re-check in case the viewport is still larger than the content (short list).\nwatch(\n  () => props.loading,\n  (now, was) => {\n    if (was && !now && props.hasMore) {\n      requestAnimationFrame(check)\n    }\n  },\n)\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"infinite-scroll\" :class=\"cn('w-full', props.class)\">\n    <!-- Reverse mode: spinner + sentinel sit above the items so new rows\n         prepend naturally without shifting the scroll position. -->\n    <template v-if=\"reverse\">\n      <div v-if=\"showSpinner\" data-slot=\"infinite-scroll-loading\" class=\"flex w-full justify-center py-3\">\n        <slot name=\"loading\">\n          <Loader2 class=\"text-muted-foreground size-5 animate-spin\" aria-label=\"Loading\" />\n        </slot>\n      </div>\n      <div ref=\"sentinel\" data-slot=\"infinite-scroll-sentinel\" class=\"h-px w-full\" aria-hidden=\"true\" />\n      <slot :items=\"items\" />\n    </template>\n\n    <template v-else>\n      <slot :items=\"items\" />\n      <div ref=\"sentinel\" data-slot=\"infinite-scroll-sentinel\" class=\"h-px w-full\" aria-hidden=\"true\" />\n      <div v-if=\"showSpinner\" data-slot=\"infinite-scroll-loading\" class=\"flex w-full justify-center py-3\">\n        <slot name=\"loading\">\n          <Loader2 class=\"text-muted-foreground size-5 animate-spin\" aria-label=\"Loading\" />\n        </slot>\n      </div>\n      <div\n        v-if=\"!hasMore && !loading\"\n        data-slot=\"infinite-scroll-end\"\n        class=\"text-muted-foreground w-full py-3 text-center text-xs\"\n      >\n        <slot name=\"end\">No more items</slot>\n      </div>\n    </template>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/infinite-scroll/InfiniteScroll.vue"
    },
    {
      "path": "packages/registry-vue/components/infinite-scroll/index.ts",
      "content": "export { default as InfiniteScroll } from './InfiniteScroll.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/infinite-scroll/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Load-more-on-scroll sentinel. Emits a `load` event when the user scrolls near the bottom (or top, in reverse mode). Supports a window or element scroll target, distance threshold, loading/hasMore/disabled gating, and slots for custom loading and end-of-list states.",
  "categories": [
    "utility",
    "data"
  ]
}