{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "infinite-scroll",
  "title": "Infinite Scroll",
  "type": "registry:ui",
  "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"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/infinite-scroll/infinite-scroll.ts",
      "content": "import { LitElement, css, html, isServer, nothing } from 'lit'\nimport { Loader2 } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\ntype ScrollEl = HTMLElement | Window | null\n\n/**\n * <uip-infinite-scroll> — Triggers a load-more event when the user scrolls near the end\n * (or top in reverse mode) of the viewport or a specified scrollable container.\n */\nexport class UipInfiniteScroll extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  static properties = {\n    hasMore: {\n      type: Boolean,\n      attribute: 'has-more',\n      converter: { fromAttribute: (v: string | null) => v !== null && v !== 'false' },\n    },\n    loading: { type: Boolean },\n    distance: { type: Number },\n    scrollTarget: { type: String, attribute: 'scroll-target' },\n    reverse: { type: Boolean },\n    disabled: { type: Boolean },\n    hideSpinner: { type: Boolean, attribute: 'hide-spinner' },\n  }\n\n  hasMore = true\n  loading = false\n  distance = 0\n  scrollTarget: string | HTMLElement = 'window'\n  reverse = false\n  disabled = false\n  hideSpinner = false\n\n  private scrollEl: ScrollEl = null\n  private boundOnScroll: (() => void) | null = null\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'infinite-scroll')\n    if (!isServer) {\n      this.attachScrollListener()\n      requestAnimationFrame(() => this.check())\n    }\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.detachScrollListener()\n  }\n\n  updated(changedProperties: Map<string, unknown>) {\n    if (changedProperties.has('scrollTarget')) {\n      this.detachScrollListener()\n      this.attachScrollListener()\n    }\n    if (changedProperties.has('loading') || changedProperties.has('hasMore')) {\n      if (!this.loading && this.hasMore) {\n        requestAnimationFrame(() => this.check())\n      }\n    }\n  }\n\n  private getScrollElement(): ScrollEl {\n    if (typeof window === 'undefined') return null\n    if (this.scrollTarget === 'window') return window\n    if (typeof this.scrollTarget === 'string') {\n      return (document.querySelector(this.scrollTarget) as HTMLElement | null) ?? window\n    }\n    return this.scrollTarget\n  }\n\n  private attachScrollListener() {\n    this.scrollEl = this.getScrollElement()\n    if (!this.scrollEl) return\n    this.boundOnScroll = () => this.check()\n    this.scrollEl.addEventListener('scroll', this.boundOnScroll, { passive: true })\n  }\n\n  private detachScrollListener() {\n    if (this.scrollEl && this.boundOnScroll) {\n      this.scrollEl.removeEventListener('scroll', this.boundOnScroll)\n    }\n    this.scrollEl = null\n    this.boundOnScroll = null\n  }\n\n  private check() {\n    if (this.disabled || this.loading || !this.hasMore) return\n    const sentinel = this.renderRoot.querySelector('[data-slot=\"infinite-scroll-sentinel\"]') as HTMLElement | null\n    if (!sentinel) return\n\n    const sentinelRect = sentinel.getBoundingClientRect()\n    let edgeTop = 0\n    let edgeBottom = window.innerHeight || document.documentElement.clientHeight\n\n    if (this.scrollEl && this.scrollEl !== window) {\n      const r = (this.scrollEl as HTMLElement).getBoundingClientRect()\n      edgeTop = r.top\n      edgeBottom = r.bottom\n    }\n\n    if (this.reverse) {\n      if (sentinelRect.bottom >= edgeTop - this.distance && sentinelRect.top <= edgeBottom) {\n        this.emitLoadMore()\n      }\n    } else {\n      if (sentinelRect.top <= edgeBottom + this.distance && sentinelRect.bottom >= edgeTop - this.distance) {\n        this.emitLoadMore()\n      }\n    }\n  }\n\n  private emitLoadMore() {\n    this.dispatchEvent(new CustomEvent('load-more', { bubbles: true, composed: true }))\n  }\n\n  render() {\n    const showSpinner = this.loading && !this.hideSpinner\n\n    const loadingSlotContent = html`\n      <slot name=\"loading\">\n        <div data-slot=\"infinite-scroll-loading\" class=\"flex w-full justify-center py-3\">\n          ${icon(Loader2, 'loader-2', 'text-muted-foreground size-5 animate-spin', 'Loading')}\n        </div>\n      </slot>\n    `\n\n    const sentinelEl = html`<div data-slot=\"infinite-scroll-sentinel\" class=\"h-px w-full\" aria-hidden=\"true\"></div>`\n\n    const endSlotContent = html`\n      <slot name=\"end\">\n        <div data-slot=\"infinite-scroll-end\" class=\"text-muted-foreground w-full py-3 text-center text-xs\">\n          No more items\n        </div>\n      </slot>\n    `\n\n    return html`\n      <div part=\"base\" class=\"w-full\">\n        ${this.reverse\n          ? html`\n              ${showSpinner ? loadingSlotContent : nothing}\n              ${sentinelEl}\n              <slot></slot>\n            `\n          : html`\n              <slot></slot>\n              ${sentinelEl}\n              ${showSpinner ? loadingSlotContent : nothing}\n              ${!this.hasMore && !this.loading ? endSlotContent : nothing}\n            `}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-infinite-scroll') || customElements.define('uip-infinite-scroll', UipInfiniteScroll)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-infinite-scroll': UipInfiniteScroll\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/infinite-scroll/infinite-scroll.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
