{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "infinite-scroll",
  "title": "Infinite Scroll",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/infinite-scroll/InfiniteScroll.tsx",
      "content": "import * as React from 'react'\nimport { Loader2 } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\nexport interface InfiniteScrollProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {\n  /** Rendered list. The component does not mutate it; the parent appends\n   *  new items in response to the `onLoadMore` callback. */\n  items?: T[]\n  /** When false, the sentinel never fires `onLoadMore` (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 `onLoadMore` 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 `onLoadMore` 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 `loadingSlot` prop instead). */\n  hideSpinner?: boolean\n  /** Override the default loading spinner. Mirrors the Vue `loading` slot. */\n  loadingSlot?: React.ReactNode\n  /** Override the default end-of-list message. Mirrors the Vue `end` slot. */\n  endSlot?: React.ReactNode\n  /** List contents. Mirrors the Vue default slot. */\n  children?: React.ReactNode\n  /** Fired when the user scrolls near the boundary. Vue `@load` -> React `onLoadMore`. */\n  onLoadMore?: () => void\n}\n\ntype ScrollEl = HTMLElement | Window | null\n\nfunction InfiniteScrollInner<T>(props: InfiniteScrollProps<T>, ref: React.Ref<HTMLDivElement>) {\n  const {\n    className,\n    hasMore = true,\n    loading = false,\n    distance = 0,\n    scrollTarget = 'window',\n    reverse = false,\n    disabled = false,\n    hideSpinner = false,\n    loadingSlot,\n    endSlot,\n    children,\n    onLoadMore,\n    ...rest\n  } = props\n\n  const sentinelRef = React.useRef<HTMLDivElement | null>(null)\n  // Latest props captured in a ref so the scroll handler never closes over\n  // stale values (it is bound once per scroll-target change).\n  const stateRef = React.useRef({ disabled, loading, hasMore, reverse, distance, onLoadMore })\n  stateRef.current = { disabled, loading, hasMore, reverse, distance, onLoadMore }\n\n  const showSpinner = loading && !hideSpinner\n\n  function getScrollElement(): ScrollEl {\n    if (scrollTarget === 'window') return typeof window === 'undefined' ? null : window\n    if (typeof scrollTarget === 'string') {\n      if (typeof document === 'undefined') return null\n      return (document.querySelector(scrollTarget) as HTMLElement | null) ?? window\n    }\n    return scrollTarget\n  }\n\n  function check() {\n    const state = stateRef.current\n    if (state.disabled || state.loading || !state.hasMore || !sentinelRef.current) return\n    const sentinelEl = sentinelRef.current\n    const sentinelRect = sentinelEl.getBoundingClientRect()\n    const viewportH = window.innerHeight || document.documentElement.clientHeight\n    const viewportW = window.innerWidth || document.documentElement.clientWidth\n    if (state.reverse) {\n      // Reverse: fire when the sentinel (anchored at top) approaches the top edge.\n      if (sentinelRect.bottom >= -state.distance && sentinelRect.top <= viewportH) {\n        state.onLoadMore?.()\n      }\n    } else {\n      // Forward: fire when the sentinel approaches the bottom edge.\n      if (sentinelRect.top <= viewportH + state.distance && sentinelRect.bottom >= -state.distance) {\n        void viewportW\n        state.onLoadMore?.()\n      }\n    }\n  }\n\n  // Bind the scroll listener; re-bind when the scroll target changes.\n  React.useEffect(() => {\n    const scrollEl = getScrollElement()\n    if (!scrollEl) return\n    const onScroll = () => check()\n    scrollEl.addEventListener('scroll', onScroll, { passive: true })\n    // Fire once on mount so an initially-empty list starts loading immediately.\n    check()\n    return () => {\n      scrollEl.removeEventListener('scroll', onScroll)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [scrollTarget])\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  React.useEffect(() => {\n    if (!loading && hasMore) {\n      const raf = requestAnimationFrame(() => check())\n      return () => cancelAnimationFrame(raf)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [loading, hasMore])\n\n  return (\n    <div data-uipkge=\"\" data-slot=\"infinite-scroll\" className={cn('w-full', className)} ref={ref} {...rest}>\n      {reverse ? (\n        <>\n          {showSpinner ? (\n            <div data-slot=\"infinite-scroll-loading\" className=\"flex w-full justify-center py-3\">\n              {loadingSlot ?? <Loader2 className=\"text-muted-foreground size-5 animate-spin\" aria-label=\"Loading\" />}\n            </div>\n          ) : null}\n          <div ref={sentinelRef} data-slot=\"infinite-scroll-sentinel\" className=\"h-px w-full\" aria-hidden=\"true\" />\n          {children}\n        </>\n      ) : (\n        <>\n          {children}\n          <div ref={sentinelRef} data-slot=\"infinite-scroll-sentinel\" className=\"h-px w-full\" aria-hidden=\"true\" />\n          {showSpinner ? (\n            <div data-slot=\"infinite-scroll-loading\" className=\"flex w-full justify-center py-3\">\n              {loadingSlot ?? <Loader2 className=\"text-muted-foreground size-5 animate-spin\" aria-label=\"Loading\" />}\n            </div>\n          ) : null}\n          {!hasMore && !loading ? (\n            <div data-slot=\"infinite-scroll-end\" className=\"text-muted-foreground w-full py-3 text-center text-xs\">\n              {endSlot ?? 'No more items'}\n            </div>\n          ) : null}\n        </>\n      )}\n    </div>\n  )\n}\n\nconst InfiniteScroll = React.forwardRef(InfiniteScrollInner) as <T = any>(\n  props: InfiniteScrollProps<T> & { ref?: React.Ref<HTMLDivElement> },\n) => React.ReactElement\nInfiniteScroll.displayName = 'InfiniteScroll'\n\nexport { InfiniteScroll }\n",
      "type": "registry:ui",
      "target": "~/components/ui/infinite-scroll/InfiniteScroll.tsx"
    },
    {
      "path": "packages/registry-react/components/infinite-scroll/index.ts",
      "content": "export { InfiniteScroll, type InfiniteScrollProps } from './InfiniteScroll'\n",
      "type": "registry:ui",
      "target": "~/components/ui/infinite-scroll/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Load-more-on-scroll sentinel. Calls an onLoadMore 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 slots for custom loading and end-of-list states.",
  "categories": [
    "utility",
    "data"
  ]
}