{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "lazy-image",
  "title": "Lazy Image",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/lazy-image/Img.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export type ImgPlaceholder = 'skeleton' | 'none'\n\n  export interface ImgProps extends HTMLAttributes<HTMLDivElement> {\n    src: string\n    srcset?: string\n    sizes?: string\n    alt: string\n    aspectRatio?: string | number\n    width?: string | number\n    height?: string | number\n    placeholder?: ImgPlaceholder\n    cover?: boolean\n    eager?: boolean\n    fallback?: string\n    transition?: boolean\n    imgClass?: string\n    /** Custom error content. Defaults to the `fallback` image, then an \"Image unavailable\" note. */\n    fallbackSnippet?: Snippet\n    onload?: (event: Event) => void\n    onerror?: (event: Event) => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { Skeleton } from '$lib/components/ui/skeleton'\n\n  let {\n    src,\n    srcset,\n    sizes,\n    alt,\n    aspectRatio,\n    width,\n    height,\n    placeholder = 'skeleton',\n    cover = true,\n    eager = false,\n    fallback,\n    transition = true,\n    class: className,\n    imgClass,\n    fallbackSnippet,\n    onload,\n    onerror,\n    ref = $bindable(null),\n    ...restProps\n  }: ImgProps = $props()\n\n  let loadState = $state<'idle' | 'loading' | 'loaded' | 'error'>('idle')\n  // svelte-ignore state_referenced_locally — the initial value is intentionally a snapshot of `eager`.\n  let visible = $state(eager)\n\n  const containerStyle = $derived.by(() => {\n    const out: Record<string, string> = {}\n    if (aspectRatio !== undefined) out.aspectRatio = String(aspectRatio)\n    if (width !== undefined) out.width = typeof width === 'number' ? `${width}px` : width\n    if (height !== undefined) out.height = typeof height === 'number' ? `${height}px` : height\n    return Object.entries(out)\n      .map(([k, v]) => `${k}: ${v}`)\n      .join('; ')\n  })\n\n  $effect(() => {\n    // Reset whenever the source changes; hold off-viewport images until near view.\n    void src\n    loadState = 'idle'\n    if (eager) {\n      visible = true\n      return\n    }\n    visible = false\n    if (typeof IntersectionObserver === 'undefined' || !ref) {\n      visible = true\n      return\n    }\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.isIntersecting) {\n            visible = true\n            observer.disconnect()\n            return\n          }\n        }\n      },\n      { rootMargin: '200px' },\n    )\n    observer.observe(ref)\n    return () => observer.disconnect()\n  })\n\n  $effect(() => {\n    if (visible && loadState === 'idle') loadState = 'loading'\n  })\n\n  function handleLoad(e: Event) {\n    loadState = 'loaded'\n    onload?.(e)\n  }\n\n  function handleError(e: Event) {\n    loadState = 'error'\n    onerror?.(e)\n  }\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"lazy-image\"\n  class={cn('bg-muted relative overflow-hidden', className)}\n  style={containerStyle}\n  {...restProps}\n>\n  {#if placeholder === 'skeleton' && loadState !== 'loaded' && loadState !== 'error'}\n    <Skeleton class=\"absolute inset-0 size-full rounded-none\" />\n  {/if}\n\n  {#if visible && loadState !== 'error'}\n    <img\n      {src}\n      {srcset}\n      {sizes}\n      {alt}\n      loading={eager ? 'eager' : 'lazy'}\n      decoding={eager ? 'sync' : 'async'}\n      class={cn(\n        'block size-full',\n        cover ? 'object-cover' : 'object-contain',\n        transition && 'transition-opacity duration-300',\n        loadState === 'loaded' ? 'opacity-100' : 'opacity-0',\n        imgClass,\n      )}\n      onload={handleLoad}\n      onerror={handleError}\n    />\n  {/if}\n\n  {#if loadState === 'error'}\n    {#if fallbackSnippet}\n      {@render fallbackSnippet()}\n    {:else if fallback}\n      <img\n        src={fallback}\n        {alt}\n        class={cn('block size-full', cover ? 'object-cover' : 'object-contain', imgClass)}\n      />\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    {/if}\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/lazy-image/Img.svelte"
    },
    {
      "path": "packages/registry-svelte/components/lazy-image/index.ts",
      "content": "export { default as Img, type ImgProps, type ImgPlaceholder } from './Img.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/lazy-image/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/skeleton.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"
  ]
}