{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lazy-image",
  "title": "Lazy Image",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/lazy-image/Img.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport { Skeleton } from '@/components/ui/skeleton'\n\ntype Placeholder = 'skeleton' | 'none'\n\nexport interface ImgProps {\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?: Placeholder\n  cover?: boolean\n  eager?: boolean\n  fallback?: string\n  transition?: boolean\n  className?: string\n  imgClassName?: string\n  /** Override the default error fallback (React equivalent of the Vue\n   *  `#fallback` named slot). */\n  fallbackContent?: React.ReactNode\n  onLoad?: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void\n  onError?: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void\n}\n\nfunction Img({\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  className,\n  imgClassName,\n  fallbackContent,\n  onLoad,\n  onError,\n}: ImgProps) {\n  const rootRef = React.useRef<HTMLDivElement | null>(null)\n  const [state, setState] = React.useState<'idle' | 'loading' | 'loaded' | 'error'>('idle')\n  const [visible, setVisible] = React.useState(eager)\n\n  const aspectStyle: React.CSSProperties = React.useMemo(() => {\n    if (aspectRatio === undefined) return {}\n    if (typeof aspectRatio === 'number') return { aspectRatio: String(aspectRatio) }\n    return { aspectRatio }\n  }, [aspectRatio])\n\n  const sizeStyle: React.CSSProperties = React.useMemo(() => {\n    const out: React.CSSProperties = {}\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 out\n  }, [width, height])\n\n  const containerStyle = { ...aspectStyle, ...sizeStyle }\n\n  // Reset + (re)observe when the source changes.\n  React.useEffect(() => {\n    setState('idle')\n    if (eager) {\n      setVisible(true)\n      return\n    }\n    setVisible(false)\n\n    if (typeof IntersectionObserver === 'undefined' || !rootRef.current) {\n      setVisible(true)\n      return\n    }\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.isIntersecting) {\n            setVisible(true)\n            observer.disconnect()\n            return\n          }\n        }\n      },\n      { rootMargin: '200px' },\n    )\n    observer.observe(rootRef.current)\n    return () => observer.disconnect()\n  }, [src, eager])\n\n  // Transition idle -> loading once the element becomes visible.\n  React.useEffect(() => {\n    if (visible) setState((s) => (s === 'idle' ? 'loading' : s))\n  }, [visible])\n\n  function handleLoad(e: React.SyntheticEvent<HTMLImageElement, Event>) {\n    setState('loaded')\n    onLoad?.(e)\n  }\n\n  function handleError(e: React.SyntheticEvent<HTMLImageElement, Event>) {\n    setState('error')\n    onError?.(e)\n  }\n\n  return (\n    <div\n      ref={rootRef}\n      data-uipkge=\"\"\n      data-slot=\"lazy-image\"\n      className={cn('bg-muted relative overflow-hidden', className)}\n      style={containerStyle}\n    >\n      {placeholder === 'skeleton' && state !== 'loaded' && state !== 'error' && (\n        <Skeleton className=\"absolute inset-0 size-full rounded-none\" />\n      )}\n\n      {visible && state !== 'error' && (\n        <img\n          src={src}\n          srcSet={srcSet}\n          sizes={sizes}\n          alt={alt}\n          loading={eager ? 'eager' : 'lazy'}\n          decoding={eager ? 'sync' : 'async'}\n          className={cn(\n            'block size-full',\n            cover ? 'object-cover' : 'object-contain',\n            transition && 'transition-opacity duration-300',\n            state === 'loaded' ? 'opacity-100' : 'opacity-0',\n            imgClassName,\n          )}\n          onLoad={handleLoad}\n          onError={handleError}\n        />\n      )}\n\n      {state === 'error' &&\n        (fallbackContent !== undefined ? (\n          fallbackContent\n        ) : fallback ? (\n          <img\n            src={fallback}\n            alt={alt}\n            className={cn('block size-full', cover ? 'object-cover' : 'object-contain', imgClassName)}\n          />\n        ) : (\n          <div\n            className=\"text-muted-foreground absolute inset-0 flex items-center justify-center text-xs\"\n            aria-label=\"Image failed to load\"\n          >\n            <span>Image unavailable</span>\n          </div>\n        ))}\n    </div>\n  )\n}\n\nexport { Img }\n",
      "type": "registry:ui",
      "target": "~/components/ui/lazy-image/Img.tsx"
    },
    {
      "path": "packages/registry-react/components/lazy-image/index.ts",
      "content": "export { Img, type ImgProps } from './Img'\n",
      "type": "registry:ui",
      "target": "~/components/ui/lazy-image/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/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"
  ]
}