{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "media",
  "title": "Media",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A picture slot: reserves the space, lazy-loads, takes a URL, an image with required alt text and a dark-mode file, or your own element, and falls back to artwork if there is no image or it fails.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/media.tsx",
      "content": "// Ballmac UI: Media. https://ui.ballmac.com/components/media\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** An image with the text that describes it. `alt` is required: use \"\" only when the picture is pure decoration. */\nexport type MediaImage = {\n  src: string\n  alt: string\n  /** Shown instead of `src` in dark mode (when an ancestor has the `dark` class). */\n  srcDark?: string\n  srcSet?: string\n  sizes?: string\n  /** Intrinsic size, so the browser can reserve space before the file arrives. */\n  width?: number\n  height?: number\n  /** CSS object-position, for example \"top\" or \"50% 20%\". */\n  position?: string\n}\n\n/** What a slot accepts: an image URL, an image with its alt text, or your own element (a next/image, a video, a component). */\nexport type MediaSource = string | MediaImage | React.ReactElement\n\nconst ASPECTS = { square: \"1 / 1\", video: \"16 / 9\", photo: \"4 / 3\", wide: \"21 / 9\", portrait: \"3 / 4\", cinema: \"2 / 1\" } as const\nexport type MediaAspect = \"auto\" | keyof typeof ASPECTS | `${number}/${number}` | `${number} / ${number}`\n\ntype MediaProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** The picture, or your own element. Leave it out to show `fallback`. */\n  media?: MediaSource | null\n  /** Describes a picture passed as a plain URL. Required for informative images; use \"\" for decoration. */\n  alt?: string\n  /** What shows when there is no `media` or the file fails to load. Usually the block's built-in artwork. */\n  fallback?: React.ReactNode\n  /** Reserves the box before the image loads, so the page does not jump. `auto` keeps the image's own proportions. */\n  aspect?: MediaAspect\n  /** `cover` fills and crops, `contain` shows all of it, `fill` stretches. */\n  fit?: \"cover\" | \"contain\" | \"fill\"\n  /** Above the fold: load now and with high priority instead of lazily. */\n  priority?: boolean\n  /** Fill a parent that has its own size (an absolutely positioned panel) instead of reserving space by aspect ratio. */\n  fill?: boolean\n  /** A card frame (rounded corners, border, soft shadow) around a real image or element. The built-in artwork keeps its own. */\n  frame?: boolean\n  /** Called when the file fails to load (the fallback is shown). */\n  onImageError?: () => void\n}\n\nconst FIT = { cover: \"object-cover\", contain: \"object-contain\", fill: \"object-fill\" } as const\n\nfunction normalize(media: MediaProps[\"media\"], alt: string | undefined): MediaImage | null {\n  if (typeof media === \"string\") {\n    if (alt === undefined && process.env.NODE_ENV !== \"production\") {\n      console.warn(`Media: \"${media}\" has no alt text. Pass alt=\"…\" to describe it, or alt=\"\" if it is decoration.`)\n    }\n    return { src: media, alt: alt ?? \"\" }\n  }\n  if (media && typeof media === \"object\" && \"src\" in media) return alt === undefined ? media : { ...media, alt }\n  return null\n}\n\n/**\n * A picture slot: it holds the space, loads lazily, falls back to the artwork if there is no image or it fails,\n * and takes a plain URL, an image with alt text (and an optional dark-mode file), or any element.\n */\nfunction Media({ media, alt, fallback, aspect = \"auto\", fit = \"cover\", priority = false, frame = false, fill: fillParent = false, onImageError, className, style, ...props }: MediaProps) {\n  const [failed, setFailed] = React.useState(false)\n  const image = normalize(media, alt)\n  const custom = !image && React.isValidElement(media) ? media : null\n  const failedKey = image ? `${image.src}|${image.srcDark ?? \"\"}` : \"\"\n  const [lastKey, setLastKey] = React.useState(failedKey)\n  if (failedKey !== lastKey) {\n    // A new file gets another chance.\n    setLastKey(failedKey)\n    setFailed(false)\n  }\n  const ratio = aspect === \"auto\" ? undefined : aspect in ASPECTS ? ASPECTS[aspect as keyof typeof ASPECTS] : aspect.replace(/\\s*\\/\\s*/, \" / \")\n  const fill = ratio !== undefined || fillParent\n  const showImage = image && !failed\n  const handleError = () => {\n    setFailed(true)\n    onImageError?.()\n  }\n  // Images that failed before React attached its handlers never fire onError again.\n  const check = React.useCallback((node: HTMLImageElement | null) => {\n    if (node && node.complete && node.naturalWidth === 0 && node.currentSrc) {\n      setFailed(true)\n      onImageError?.()\n    }\n  }, [onImageError])\n  const img = (src: string, extra?: string) => (\n    <img\n      ref={check}\n      src={src}\n      alt={image!.alt}\n      srcSet={src === image!.src ? image!.srcSet : undefined}\n      sizes={image!.sizes}\n      width={image!.width}\n      height={image!.height}\n      loading={priority ? \"eager\" : \"lazy\"}\n      decoding=\"async\"\n      fetchPriority={priority ? \"high\" : \"auto\"}\n      onError={handleError}\n      style={image!.position ? { objectPosition: image!.position } : undefined}\n      className={cn(fill ? \"absolute inset-0 size-full\" : \"block h-auto w-full\", FIT[fit], extra)}\n    />\n  )\n  return (\n    <div\n      data-slot=\"media\"\n      data-state={showImage ? \"image\" : custom ? \"custom\" : \"fallback\"}\n      className={cn(\n        \"relative overflow-hidden data-[state=fallback]:overflow-visible\",\n        fill && \"bg-muted/40 data-[state=fallback]:bg-transparent\",\n        frame && \"data-[state=custom]:rounded-2xl data-[state=custom]:border data-[state=custom]:shadow-[0_30px_80px_-40px_rgb(0_0_0/0.35)] data-[state=image]:rounded-2xl data-[state=image]:border data-[state=image]:shadow-[0_30px_80px_-40px_rgb(0_0_0/0.35)]\",\n        custom && \"[&>img]:absolute [&>img]:inset-0 [&>img]:size-full [&>img]:object-cover\",\n        className\n      )}\n      style={{ ...(ratio ? { aspectRatio: ratio } : {}), ...style }}\n      {...props}\n    >\n      {showImage ? (\n        image.srcDark ? (\n          <>\n            {img(image.src, \"dark:hidden\")}\n            {img(image.srcDark, \"hidden dark:block\")}\n          </>\n        ) : (\n          img(image.src)\n        )\n      ) : custom ? (\n        custom\n      ) : (\n        fallback ?? null\n      )}\n    </div>\n  )\n}\n\n/** True for an image URL or a `{ src, alt }` object, false for an element or nothing: lets a slot that used to take any element accept pictures too. */\nfunction isMediaImage(value: unknown): value is string | MediaImage {\n  return typeof value === \"string\" || (typeof value === \"object\" && value !== null && !React.isValidElement(value) && \"src\" in value)\n}\n\nexport { Media, isMediaImage, type MediaProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/media.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "image",
      "picture",
      "slot",
      "screenshot",
      "photo",
      "alt text",
      "aspect ratio",
      "dark mode"
    ],
    "version": "1.0.0",
    "updated": "2026-10-02",
    "ai": {
      "summary": "Pass media as a URL (with alt), an object { src, alt, srcDark? }, or any element such as a next/image. aspect reserves the box (video, photo, square, wide, portrait or '3/2'); fit is cover | contain | fill; priority loads an above-the-fold image eagerly. fallback shows when there is no media or the file fails.",
      "whenToUse": [
        "Any place a product screenshot, photo, cover or avatar goes",
        "Blocks and templates that ship with generated artwork but should accept the buyer's own image"
      ],
      "whenNotToUse": [
        "Icons (use an icon component)",
        "Animated or interactive content that is not an image: pass it as an element"
      ],
      "composesWith": [
        "hero-1",
        "features-5",
        "blog-1"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "Informative images need alt text; alt=\"\" marks decoration. A URL without alt logs a warning in development"
        },
        {
          "keys": "Layout",
          "action": "A fixed aspect ratio reserves the space, so nothing jumps when the file arrives"
        }
      ],
      "customization": [
        "aspect, fit and priority",
        "srcDark for a dark-mode file",
        "position for object-position",
        "fallback for the artwork shown with no image",
        "onImageError"
      ]
    },
    "examples": [
      "media-demo",
      "media-dark"
    ],
    "url": "https://ui.ballmac.com/components/media"
  },
  "categories": [
    "primitives"
  ],
  "type": "registry:ui"
}