{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "lazy-image",
  "title": "Lazy Image",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/lazy-image/lazy-image.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  TemplateRef,\n  ViewChild,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiSkeletonComponent } from '@/ui/skeleton/skeleton.component'\nimport { UiRenderTemplateDirective } from '@/ui/popper/popper'\n\nexport type LazyImagePlaceholder = 'skeleton' | 'none'\nexport type LazyImageState = 'idle' | 'loading' | 'loaded' | 'error'\n\n/**\n * Angular port of UIPKGE LazyImage (React `Img`). Lazy-loaded image with aspect-ratio\n * reservation, Skeleton placeholder, fade-in and error fallback. The <img> mounts once the host\n * comes within 200px of the viewport (IntersectionObserver; renders immediately without IO or\n * with `eager`). React's `fallbackContent` node is a TemplateRef input.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-lazy-image, [ui-lazy-image]',\n  standalone: true,\n  imports: [UiSkeletonComponent, UiRenderTemplateDirective],\n  host: {\n    '[attr.data-slot]': '\"lazy-image\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[style.aspect-ratio]': 'aspectRatioStyle',\n    '[style.width]': 'widthStyle',\n    '[style.height]': 'heightStyle',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-container #anchor />\n    @if (placeholder === 'skeleton' && state() !== 'loaded' && state() !== 'error') {\n      <div ui-skeleton class=\"absolute inset-0 size-full rounded-none\"></div>\n    }\n    @if (visible() && state() !== 'error') {\n      <img\n        [src]=\"src\"\n        [attr.srcset]=\"srcSet ?? null\"\n        [attr.sizes]=\"sizes ?? null\"\n        [alt]=\"alt\"\n        [attr.loading]=\"eager ? 'eager' : 'lazy'\"\n        [attr.decoding]=\"eager ? 'sync' : 'async'\"\n        [class]=\"imgClassMerged\"\n        (load)=\"onLoad($event)\"\n        (error)=\"onError($event)\"\n      />\n    }\n    @if (state() === 'error') {\n      @if (fallbackContent) {\n        <ng-container [uiRenderTemplate]=\"fallbackContent\" />\n      } @else if (fallback) {\n        <img [src]=\"fallback\" [alt]=\"alt\" [class]=\"fallbackClass\" />\n      } @else {\n        <div\n          role=\"img\"\n          class=\"text-muted-foreground absolute inset-0 flex items-center justify-center text-xs\"\n          aria-label=\"Image failed to load\"\n        >\n          <span aria-hidden=\"true\">Image unavailable</span>\n        </div>\n      }\n    }`,\n})\nexport class UiLazyImageComponent implements OnChanges, AfterViewInit, OnDestroy {\n  @Input() src = ''\n  @Input() srcSet?: string\n  @Input() sizes?: string\n  @Input() alt = ''\n  @Input() aspectRatio?: string | number\n  @Input() width?: string | number\n  @Input() height?: string | number\n  @Input() placeholder: LazyImagePlaceholder = 'skeleton'\n  @Input({ transform: booleanAttribute }) cover = true\n  @Input({ transform: booleanAttribute }) eager = false\n  @Input() fallback?: string\n  @Input({ transform: booleanAttribute }) transition = true\n  @Input('class') className?: string\n  @Input() imgClassName?: string\n  /** Overrides the default error fallback (React `fallbackContent`). */\n  @Input() fallbackContent?: TemplateRef<unknown>\n\n  /** React `onLoad` / `onError` (img load/error events don't bubble, so no native collision). */\n  @Output() load = new EventEmitter<Event>()\n  @Output() error = new EventEmitter<Event>()\n\n  /** Comment node inside the host; its parent is the host element (no ElementRef injection needed). */\n  @ViewChild('anchor', { read: ElementRef }) private anchor?: ElementRef<Comment>\n\n  readonly state = signal<LazyImageState>('idle')\n  readonly visible = signal(false)\n  private observer?: IntersectionObserver\n  private viewReady = false\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['src'] || changes['eager']) {\n      this.state.set('idle')\n      if (this.viewReady) this.observe()\n      else if (this.eager) this.show()\n    }\n  }\n\n  ngAfterViewInit(): void {\n    this.viewReady = true\n    this.observe()\n  }\n\n  ngOnDestroy(): void {\n    this.observer?.disconnect()\n  }\n\n  /** Reset + (re)observe, like React's [src, eager] effect. */\n  private observe(): void {\n    this.observer?.disconnect()\n    this.observer = undefined\n    if (this.eager) return this.show()\n    this.visible.set(false)\n    const host = this.anchor?.nativeElement.parentElement\n    if (typeof IntersectionObserver === 'undefined' || !host) return this.show()\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (entries.some((e) => e.isIntersecting)) {\n          observer.disconnect()\n          this.show()\n        }\n      },\n      { rootMargin: '200px' },\n    )\n    this.observer = observer\n    observer.observe(host)\n  }\n\n  private show(): void {\n    this.visible.set(true)\n    if (this.state() === 'idle') this.state.set('loading')\n  }\n\n  get hostClass(): string {\n    return cn('block bg-muted relative overflow-hidden', this.className)\n  }\n\n  get imgClassMerged(): string {\n    return cn(\n      'block size-full',\n      this.cover ? 'object-cover' : 'object-contain',\n      this.transition && 'transition-opacity duration-300',\n      this.state() === 'loaded' ? 'opacity-100' : 'opacity-0',\n      this.imgClassName,\n    )\n  }\n\n  get fallbackClass(): string {\n    return cn('block size-full', this.cover ? 'object-cover' : 'object-contain', this.imgClassName)\n  }\n\n  get aspectRatioStyle(): string | null {\n    return this.aspectRatio === undefined ? null : String(this.aspectRatio)\n  }\n\n  get widthStyle(): string | null {\n    if (this.width === undefined) return null\n    return typeof this.width === 'number' ? `${this.width}px` : this.width\n  }\n\n  get heightStyle(): string | null {\n    if (this.height === undefined) return null\n    return typeof this.height === 'number' ? `${this.height}px` : this.height\n  }\n\n  onLoad(e: Event): void {\n    this.state.set('loaded')\n    this.load.emit(e)\n  }\n\n  onError(e: Event): void {\n    this.state.set('error')\n    this.error.emit(e)\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/lazy-image/lazy-image.component.ts"
    },
    {
      "path": "packages/registry-angular/components/lazy-image/index.ts",
      "content": "export { UiLazyImageComponent } from './lazy-image.component'\nexport type { LazyImagePlaceholder, LazyImageState } from './lazy-image.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/lazy-image/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/skeleton.json",
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Lazy-loaded image with aspect-ratio reservation, skeleton placeholder, fade-in transition, and error fallback. Composes loading=\"lazy\" with IntersectionObserver for off-viewport hold and pairs with the skeleton primitive for the placeholder state.",
  "categories": [
    "data-display"
  ]
}