{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image",
  "title": "Image",
  "description": "An image on the M3 shape scale, with aspect ratio, fallback, and loading states.",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/icon.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/materialcn-theme.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/image.tsx",
      "content": "import { useState } from \"react\"\nimport { ImageOffIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Icon } from \"./icon\"\nimport { imageVariants } from \"./image-variants\"\nimport type { ImageProps } from \"./image.types\"\n\n/**\n * An image that holds its space, degrades deliberately, and serves an asset\n * matched to the screen.\n *\n * The box is reserved by the wrapper, from `width`/`height` or `aspectRatio`,\n * before the image exists. Letting the `img` size itself is what produces the\n * classic reflow: the page lays out at zero height, then jumps when the bytes\n * land — and jumps again for every image below it.\n */\nfunction Image({\n  alt,\n  aspectRatio,\n  className,\n  crossOrigin,\n  decorative,\n  decoding,\n  densities,\n  fallback,\n  fetchPriority,\n  height,\n  loading,\n  onError,\n  onLoad,\n  referrerPolicy,\n  shape,\n  sizes,\n  src,\n  style,\n  width,\n  ...props\n}: ImageProps) {\n  const [failed, setFailed] = useState(false)\n\n  /*\n   * A new source deserves a fresh attempt, or one broken image poisons the\n   * slot for every image that later occupies it. Adjusting during render\n   * rather than in an effect: an effect would paint one frame of the previous\n   * image's failure state against the new source.\n   */\n  const [attempted, setAttempted] = useState(src)\n  if (attempted !== src) {\n    setAttempted(src)\n    setFailed(false)\n  }\n\n  const srcSet = densities\n    ? Object.entries(densities)\n        .map(([ratio, url]) => `${url} ${ratio}x`)\n        .join(\", \")\n    : undefined\n\n  const showFallback = !src || failed\n\n  /*\n   * The remaining props land on the *wrapper*, not on the `img`.\n   *\n   * The wrapper is the thing that exists in every state; the `img` is not\n   * there at all while the fallback is showing. Putting an `id`, a\n   * `data-testid`, or an `aria-describedby` on the image would make it come\n   * and go with the bytes — so only genuinely image-loading attributes are\n   * destructured above and forwarded inward.\n   */\n  return (\n    <span\n      data-slot=\"image\"\n      data-state={showFallback ? \"fallback\" : \"loaded\"}\n      className={cn(imageVariants({ shape }), className)}\n      style={{ width, height, aspectRatio, ...style }}\n      {...props}\n    >\n      {showFallback ? (\n        fallback === false ? null : (\n          <span\n            data-slot=\"image-fallback\"\n            // Decorative whatever the image was: the `alt` text describes a\n            // picture that is not there, and reading it out would describe\n            // something the user cannot see.\n            aria-hidden\n            className=\"text-m3-on-surface-variant absolute inset-0 grid place-items-center\"\n          >\n            {fallback ?? (\n              <Icon size=\"md\">\n                <ImageOffIcon />\n              </Icon>\n            )}\n          </span>\n        )\n      ) : (\n        <img\n          data-slot=\"image-img\"\n          src={src}\n          srcSet={srcSet}\n          sizes={sizes}\n          alt={decorative ? \"\" : alt}\n          aria-hidden={decorative || undefined}\n          loading={loading}\n          decoding={decoding}\n          fetchPriority={fetchPriority}\n          crossOrigin={crossOrigin}\n          referrerPolicy={referrerPolicy}\n          // The wrapper owns the box; the image fills it and crops rather than\n          // stretching, so a wrong aspect ratio is a crop and not a distortion.\n          className=\"size-full object-cover\"\n          onLoad={onLoad}\n          onError={(event) => {\n            setFailed(true)\n            onError?.(event)\n          }}\n        />\n      )}\n    </span>\n  )\n}\n\nexport { Image, imageVariants }\nexport type { ImageProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "src/components/ui/image-variants.ts",
      "content": "/**\n * Variant definitions live beside the component rather than inside it — see\n * badge-variants.ts for why (React Fast Refresh boundaries).\n */\n\nimport { cva } from \"class-variance-authority\"\n\n/**\n * The mask lives on a wrapper rather than on the `img` itself, so the corner\n * step applies to the fallback too. An image whose placeholder is square and\n * whose photograph is rounded changes shape at the moment it loads, which is\n * the same jump the reserved box exists to prevent.\n */\nexport const imageVariants = cva(\n  \"relative block overflow-hidden bg-m3-surface-container-highest\",\n  {\n    variants: {\n      shape: {\n        none: \"rounded-m3-none\",\n        xs: \"rounded-m3-xs\",\n        sm: \"rounded-m3-sm\",\n        md: \"rounded-m3-md\",\n        lg: \"rounded-m3-lg\",\n        xl: \"rounded-m3-xl\",\n        \"2xl\": \"rounded-m3-2xl\",\n        full: \"rounded-m3-full\",\n      },\n    },\n    defaultVariants: {\n      shape: \"none\",\n    },\n  }\n)\n",
      "type": "registry:ui"
    },
    {
      "path": "src/components/ui/image.types.ts",
      "content": "import type * as React from \"react\"\n\n/** The shape steps an image can be masked to. */\nexport type ImageShape =\n  | \"none\"\n  | \"xs\"\n  | \"sm\"\n  | \"md\"\n  | \"lg\"\n  | \"xl\"\n  | \"2xl\"\n  | \"full\"\n\ntype Base = Omit<React.ComponentProps<\"img\">, \"alt\" | \"srcSet\"> & {\n  /**\n   * Additional sources by device pixel ratio — `{ 2: \"…@2x.png\" }`. Emitted as\n   * `srcset`, so the browser picks by the viewer's actual screen rather than by\n   * anything this component would have to guess.\n   */\n  densities?: Partial<Record<2 | 3, string>>\n  /**\n   * Holds the box before the image arrives. Either give both `width` and\n   * `height`, or an `aspectRatio` that scales against the parent's width.\n   */\n  aspectRatio?: React.CSSProperties[\"aspectRatio\"]\n  /** Masks the image, using the library's corner steps. */\n  shape?: ImageShape\n  /**\n   * Drawn when `src` is missing or fails to load. Defaults to a placeholder\n   * surface with an icon; pass your own node, or `false` for an empty box.\n   */\n  fallback?: React.ReactNode | false\n}\n\n/**\n * The alt text is required, and deliberately so — an image with no `alt` at all\n * is announced by its file name, which is the worst of the three outcomes.\n */\ntype Meaningful = Base & { alt: string; decorative?: false }\n\n/**\n * A decorative image says nothing a screen reader needs. Marking it is not the\n * same as forgetting to: this sets `alt=\"\"` and hides it, which is what tells\n * assistive technology to skip it rather than to guess.\n */\ntype Decorative = Base & { decorative: true; alt?: never }\n\nexport type ImageProps = Meaningful | Decorative\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "media"
  ],
  "type": "registry:ui"
}