{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "infinite-scroll",
  "type": "registry:ui",
  "title": "Infinite scroll",
  "description": "Load the next page when a sentinel scrolls into view, with a loading row, an end of list message and a Load more button as the accessible fallback.",
  "dependencies": [],
  "registryDependencies": [
    "https://ui.minidev.pro/r/button.json",
    "https://ui.minidev.pro/r/spinner.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/infinite-scroll.tsx",
      "type": "registry:ui",
      "target": "components/ui/infinite-scroll.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { Button } from \"@/components/ui/button\"\n\n/**\n * Loads more items when a sentinel near the end of the list scrolls into\n * view, using IntersectionObserver. It shows a loading row, an end of list\n * message and a real \"Load more\" button as the accessible fallback, and it\n * never fires twice for the same page.\n */\ntype InfiniteScrollProps<T> = {\n  /** Fetch the next page; return the items and whether more remain. */\n  loadMore?: (page: number) => Promise<{ items: T[]; hasMore: boolean }>\n  renderItem?: (item: T, index: number) => React.ReactNode\n  /** Start loading this many pixels before the end. */\n  rootMargin?: string\n  className?: string\n}\n\ntype DemoItem = { id: number; title: string }\n\nconst demoLoad = (page: number) =>\n  new Promise<{ items: DemoItem[]; hasMore: boolean }>((r) =>\n    setTimeout(() => r({ items: Array.from({ length: 8 }, (_, i) => ({ id: page * 8 + i + 1, title: `Order #${1040 + page * 8 + i}` })), hasMore: page < 3 }), 700),\n  )\n\nfunction InfiniteScroll<T = DemoItem>({ loadMore, renderItem, rootMargin = \"240px\", className }: InfiniteScrollProps<T>) {\n  const load = (loadMore ?? (demoLoad as unknown as (p: number) => Promise<{ items: T[]; hasMore: boolean }>))\n  const [items, setItems] = React.useState<T[]>([])\n  const [page, setPage] = React.useState(0)\n  const [loading, setLoading] = React.useState(false)\n  const [hasMore, setHasMore] = React.useState(true)\n  const sentinel = React.useRef<HTMLDivElement>(null)\n  const busy = React.useRef(false)\n  const scroller = React.useRef<HTMLDivElement>(null)\n\n  const next = React.useCallback(async () => {\n    if (busy.current || !hasMore) return\n    busy.current = true\n    setLoading(true)\n    try {\n      const res = await load(page)\n      setItems((prev) => [...prev, ...res.items])\n      setHasMore(res.hasMore)\n      setPage((p) => p + 1)\n    } finally {\n      busy.current = false\n      setLoading(false)\n    }\n  }, [load, page, hasMore])\n\n  React.useEffect(() => {\n    const el = sentinel.current\n    if (!el) return\n    const io = new IntersectionObserver(([e]) => e.isIntersecting && next(), { root: scroller.current, rootMargin })\n    io.observe(el)\n    return () => io.disconnect()\n  }, [next, rootMargin])\n\n  const render =\n    renderItem ??\n    ((it: T) => {\n      const d = it as unknown as DemoItem\n      return (\n        <div className=\"flex items-center justify-between rounded-lg border border-border bg-surface px-3 py-2.5 text-sm shadow-xs\">\n          <span className=\"text-fg\">{d.title}</span>\n          <span className=\"font-mono text-xs text-fg-subtle\">#{d.id}</span>\n        </div>\n      )\n    })\n\n  return (\n    <div ref={scroller} data-slot=\"infinite-scroll\" className={cn(\"h-80 w-full max-w-sm overflow-y-auto rounded-2xl border border-border bg-sunken p-3\", className)}>\n      <ul className=\"space-y-2\" aria-busy={loading}>\n        {items.map((it, i) => (\n          <li key={i}>{render(it, i)}</li>\n        ))}\n      </ul>\n      <div ref={sentinel} aria-hidden className=\"h-px\" />\n      <div className=\"flex justify-center py-3 text-xs text-fg-muted\" aria-live=\"polite\">\n        {loading ? (\n          <span className=\"flex items-center gap-2\"><Spinner className=\"size-3.5\" /> Loading more</span>\n        ) : hasMore ? (\n          <Button size=\"sm\" variant=\"ghost\" onClick={next}>Load more</Button>\n        ) : (\n          <span>You have reached the end</span>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { InfiniteScroll }\nexport type { InfiniteScrollProps }\n"
    }
  ]
}
