{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "infinite-scroll",
  "title": "Infinite Scroll",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/infinite-scroll/infinite-scroll.component.ts",
      "content": "import {\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  Output,\n  ViewChild,\n  AfterViewInit,\n  OnDestroy,\n  OnChanges,\n  SimpleChanges,\n  booleanAttribute,\n  inject,\n  ChangeDetectorRef,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\n/**\n * Angular port of UIPKGE InfiniteScroll. Load-more-on-scroll sentinel with\n * window/element target, threshold, reverse mode, loading/hasMore gating.\n * Classes and DOM structure mirror Vue and React sources.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-infinite-scroll, [ui-infinite-scroll]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"infinite-scroll\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (reverse) {\n      @if (showSpinner) {\n        <div data-slot=\"infinite-scroll-loading\" class=\"flex w-full justify-center py-3\">\n          <ng-content select=\"[slot=loading]\">\n            <svg\n              class=\"text-muted-foreground size-5 animate-spin\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              aria-label=\"Loading\"\n            >\n              <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n            </svg>\n          </ng-content>\n        </div>\n      }\n      <div #sentinel data-slot=\"infinite-scroll-sentinel\" aria-hidden=\"true\" class=\"h-px w-full\"></div>\n      <ng-content />\n    } @else {\n      <ng-content />\n      <div #sentinel data-slot=\"infinite-scroll-sentinel\" aria-hidden=\"true\" class=\"h-px w-full\"></div>\n      @if (showSpinner) {\n        <div data-slot=\"infinite-scroll-loading\" class=\"flex w-full justify-center py-3\">\n          <ng-content select=\"[slot=loading]\">\n            <svg\n              class=\"text-muted-foreground size-5 animate-spin\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              aria-label=\"Loading\"\n            >\n              <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n            </svg>\n          </ng-content>\n        </div>\n      }\n      @if (!hasMore && !loading) {\n        <div data-slot=\"infinite-scroll-end\" class=\"text-muted-foreground w-full py-3 text-center text-xs\">\n          <ng-content select=\"[slot=end]\">No more items</ng-content>\n        </div>\n      }\n    }\n  `,\n})\nexport class UiInfiniteScrollComponent implements AfterViewInit, OnDestroy, OnChanges {\n  private cdr?: ChangeDetectorRef\n\n  constructor() {\n    try {\n      this.cdr = inject(ChangeDetectorRef, { optional: true }) ?? undefined\n    } catch {\n      // Instantiated outside injection context (e.g. unit tests)\n    }\n  }\n\n  @Input() hasMore = true\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input() distance = 0\n  @Input() scrollTarget: 'window' | HTMLElement | string = 'window'\n  @Input({ transform: booleanAttribute }) reverse = false\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) hideSpinner = false\n  @Input('class') className?: string\n\n  @Output() load = new EventEmitter<void>()\n  @Output() onLoadMore = this.load\n  @Output() loadMore = this.load\n\n  @ViewChild('sentinel') sentinelRef?: ElementRef<HTMLElement>\n\n  private scrollEl: HTMLElement | Window | null = null\n\n  get hostClass(): string {\n    return cn('block w-full', this.className)\n  }\n\n  get showSpinner(): boolean {\n    return this.loading && !this.hideSpinner\n  }\n\n  get canLoad(): boolean {\n    return !this.disabled && !this.loading && this.hasMore\n  }\n\n  checkAndEmit(nearEdge: boolean): void {\n    if (nearEdge && this.canLoad) this.load.emit()\n  }\n\n  onIntersection(isIntersecting: boolean): void {\n    this.checkAndEmit(isIntersecting)\n  }\n\n  private getScrollElement(): HTMLElement | Window | null {\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 check(): void {\n    if (!this.canLoad || !this.sentinelRef?.nativeElement) return\n    const sentinelEl = this.sentinelRef.nativeElement\n    const sentinelRect = sentinelEl.getBoundingClientRect()\n\n    let edgeTop = 0\n    let edgeBottom = typeof window !== 'undefined' ? window.innerHeight || document.documentElement.clientHeight : 0\n\n    if (this.scrollEl && typeof window !== 'undefined' && 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.load.emit()\n      }\n    } else {\n      if (sentinelRect.top <= edgeBottom + this.distance && sentinelRect.bottom >= edgeTop - this.distance) {\n        this.load.emit()\n      }\n    }\n  }\n\n  private onScroll = (): void => {\n    this.check()\n  }\n\n  ngAfterViewInit(): void {\n    this.bindScroll()\n    this.check()\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['scrollTarget'] && !changes['scrollTarget'].firstChange) {\n      this.unbindScroll()\n      this.bindScroll()\n    }\n    if (changes['loading']) {\n      this.cdr?.markForCheck()\n      if (!changes['loading'].currentValue && this.hasMore) {\n        if (typeof requestAnimationFrame !== 'undefined') {\n          requestAnimationFrame(() => this.check())\n        }\n      }\n    }\n    if (changes['hasMore']) {\n      this.cdr?.markForCheck()\n    }\n  }\n\n  private bindScroll(): void {\n    this.scrollEl = this.getScrollElement()\n    this.scrollEl?.addEventListener('scroll', this.onScroll, { passive: true })\n  }\n\n  private unbindScroll(): void {\n    this.scrollEl?.removeEventListener('scroll', this.onScroll)\n    this.scrollEl = null\n  }\n\n  ngOnDestroy(): void {\n    this.unbindScroll()\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/infinite-scroll/infinite-scroll.component.ts"
    },
    {
      "path": "packages/registry-angular/components/infinite-scroll/index.ts",
      "content": "export { UiInfiniteScrollComponent } from './infinite-scroll.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/infinite-scroll/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "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"
  ]
}