{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "infinite-scroll",
  "title": "Infinite Scroll",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/infinite-scroll/InfiniteScroll.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface InfiniteScrollProps<T = unknown> extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n    /** Rendered list. The component does not mutate it; the parent appends\n     *  new items in response to the `onload` callback. */\n    items?: T[]\n    /** When false, the sentinel never fires `onload` (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 `onload` fires. Larger = earlier. */\n    distance?: number\n    /** Scroll container. `\"window\"` listens on the viewport; pass an element\n     *  (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 `onload` 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` snippet instead). */\n    hideSpinner?: boolean\n    /** Row content. Receives the current `items`. */\n    children?: Snippet<[{ items: T[] }]>\n    /** Custom loading indicator. */\n    loadingSnippet?: Snippet\n    /** Custom end-of-list content. */\n    endSnippet?: Snippet\n    /** Fired when the sentinel crosses the distance threshold. */\n    onload?: () => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\" generics=\"T\">\n  import { untrack } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { Loader2 } from '@lucide/svelte'\n\n  let {\n    class: className,\n    items = [],\n    hasMore = true,\n    loading = false,\n    distance = 0,\n    scrollTarget = 'window',\n    reverse = false,\n    disabled = false,\n    hideSpinner = false,\n    children,\n    loadingSnippet,\n    endSnippet,\n    onload,\n    ref = $bindable(null),\n    ...restProps\n  }: InfiniteScrollProps<T> = $props()\n\n  let sentinel: HTMLElement | null = $state(null)\n  let scrollEl: HTMLElement | Window | null = null\n\n  const showSpinner = $derived(loading && !hideSpinner)\n\n  function getScrollElement(): HTMLElement | Window | null {\n    if (scrollTarget === 'window') return window\n    if (typeof scrollTarget === 'string') {\n      return (document.querySelector(scrollTarget) as HTMLElement | null) ?? window\n    }\n    return scrollTarget\n  }\n\n  function check() {\n    if (disabled || loading || !hasMore || !sentinel) return\n    const sentinelRect = sentinel.getBoundingClientRect()\n    // When listening on a scrollable element, use that element's visible box —\n    // not the window — otherwise a short/offset container never (or always)\n    // trips the threshold relative to the viewport.\n    let edgeTop = 0\n    let edgeBottom = window.innerHeight || document.documentElement.clientHeight\n    if (scrollEl && scrollEl !== window) {\n      const r = (scrollEl as HTMLElement).getBoundingClientRect()\n      edgeTop = r.top\n      edgeBottom = r.bottom\n    }\n    if (reverse) {\n      // Reverse: fire when the sentinel (anchored at top) approaches the top edge.\n      if (sentinelRect.bottom >= edgeTop - distance && sentinelRect.top <= edgeBottom) {\n        onload?.()\n      }\n    } else {\n      // Forward: fire when the sentinel approaches the bottom edge.\n      if (sentinelRect.top <= edgeBottom + distance && sentinelRect.bottom >= edgeTop - distance) {\n        onload?.()\n      }\n    }\n  }\n\n  function onScroll() {\n    check()\n  }\n\n  $effect(() => {\n    // Re-bind the listener when the scroll target changes.\n    void scrollTarget\n    scrollEl?.removeEventListener('scroll', onScroll)\n    scrollEl = getScrollElement()\n    scrollEl?.addEventListener('scroll', onScroll, { passive: true })\n    // Fire once on mount so an initially-empty list starts loading immediately.\n    // Untracked so gating-prop changes don't re-bind the listener.\n    untrack(check)\n    return () => {\n      scrollEl?.removeEventListener('scroll', onScroll)\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).\n  let wasLoading = $state(false)\n  $effect(() => {\n    const now = loading\n    if (wasLoading && !now && hasMore) {\n      requestAnimationFrame(check)\n    }\n    wasLoading = now\n  })\n</script>\n\n<div bind:this={ref} data-uipkge=\"\" data-slot=\"infinite-scroll\" class={cn('w-full', className)} {...restProps}>\n  <!-- Reverse mode: spinner + sentinel sit above the items so new rows\n       prepend naturally without shifting the scroll position. -->\n  {#if reverse}\n    {#if showSpinner}\n      <div data-slot=\"infinite-scroll-loading\" class=\"flex w-full justify-center py-3\">\n        {#if loadingSnippet}\n          {@render loadingSnippet()}\n        {:else}\n          <Loader2 class=\"text-muted-foreground size-5 animate-spin\" aria-label=\"Loading\" />\n        {/if}\n      </div>\n    {/if}\n    <div bind:this={sentinel} data-slot=\"infinite-scroll-sentinel\" class=\"h-px w-full\" aria-hidden=\"true\"></div>\n    {@render children?.({ items })}\n  {:else}\n    {@render children?.({ items })}\n    <div bind:this={sentinel} data-slot=\"infinite-scroll-sentinel\" class=\"h-px w-full\" aria-hidden=\"true\"></div>\n    {#if showSpinner}\n      <div data-slot=\"infinite-scroll-loading\" class=\"flex w-full justify-center py-3\">\n        {#if loadingSnippet}\n          {@render loadingSnippet()}\n        {:else}\n          <Loader2 class=\"text-muted-foreground size-5 animate-spin\" aria-label=\"Loading\" />\n        {/if}\n      </div>\n    {/if}\n    {#if !hasMore && !loading}\n      <div data-slot=\"infinite-scroll-end\" class=\"text-muted-foreground w-full py-3 text-center text-xs\">\n        {#if endSnippet}\n          {@render endSnippet()}\n        {:else}\n          No more items\n        {/if}\n      </div>\n    {/if}\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/infinite-scroll/InfiniteScroll.svelte"
    },
    {
      "path": "packages/registry-svelte/components/infinite-scroll/index.ts",
      "content": "export { default as InfiniteScroll, type InfiniteScrollProps } from './InfiniteScroll.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/infinite-scroll/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Load-more-on-scroll sentinel. Fires an `onload` callback 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 snippets for custom loading and end-of-list states.",
  "categories": [
    "utility",
    "data"
  ]
}