{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "figure",
  "title": "Figure",
  "description": "A picture cropped to a ratio and marked like a printer's proof, its number, caption and credit set in type underneath or in the margin beside it.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/aspect-ratio.json",
    "https://thedirectors.agency/ui/r/meta.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/figure.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { AspectRatio } from \"@/components/ui/aspect-ratio\"\nimport { Meta } from \"@/components/ui/meta\"\n\ntype FigureProps = Omit<React.ComponentProps<\"figure\">, \"children\"> & {\n  src: string\n  /** What the picture shows. Read aloud, and set in the frame in its place if the picture can't be loaded. */\n  alt: string\n  srcSet?: string\n  sizes?: string\n  /** Width over height of the crop. 3 / 2 by default, the 35mm negative. Change it and the picture recrops as the frame eases. */\n  ratio?: number\n  /** Where the crop holds the picture, as CSS object-position. */\n  position?: string\n  /** The figure's number, set as data before the caption: 01. */\n  number?: React.ReactNode\n  caption?: React.ReactNode\n  /** Who made it, spread to the far end of the caption row. */\n  credit?: React.ReactNode\n  /** plate: the caption under the picture. margin: the caption in the margin beside it, the number at its head. */\n  variant?: \"plate\" | \"margin\"\n  loading?: \"lazy\" | \"eager\"\n}\n\n/** A picture cropped to a ratio and marked like a printer's proof, with its number, caption and credit set as type. */\nfunction Figure({ src, alt, srcSet, sizes, ratio = 3 / 2, position, number, caption, credit, variant = \"plate\", loading = \"lazy\", className, ...props }: FigureProps) {\n  const img = React.useRef<HTMLImageElement>(null)\n  // A picture that failed before hydration fires no error event we can hear: look once it's mounted.\n  React.useEffect(() => {\n    const el = img.current\n    if (el) el.closest(\"figure\")?.toggleAttribute(\"data-missing\", el.complete && el.naturalWidth === 0)\n  }, [src])\n  const mark = (on: boolean) => (e: React.SyntheticEvent<HTMLImageElement>) => e.currentTarget.closest(\"figure\")?.toggleAttribute(\"data-missing\", on)\n\n  return (\n    <figure data-slot=\"figure\" data-variant={variant} className={cn(\"ot-figure\", className)} {...props}>\n      <AspectRatio ratio={ratio} className=\"ot-figure-frame\">\n        {/* eslint-disable-next-line @next/next/no-img-element -- a registry item is framework-agnostic */}\n        <img\n          ref={img}\n          src={src}\n          srcSet={srcSet}\n          sizes={sizes}\n          alt={alt}\n          loading={loading}\n          decoding=\"async\"\n          className=\"ot-figure-img\"\n          style={position ? { objectPosition: position } : undefined}\n          onLoad={mark(false)}\n          onError={mark(true)}\n        />\n        {alt ? (\n          <span className=\"ot-figure-alt\" aria-hidden=\"true\">\n            {alt}\n          </span>\n        ) : null}\n      </AspectRatio>\n      {number != null || caption || credit ? (\n        <figcaption className=\"ot-figure-caption\">\n          {number != null ? (\n            <span className=\"ot-figure-number\">\n              <span className=\"ot-sr\">Figure </span>\n              {number}\n            </span>\n          ) : null}\n          <Meta className=\"ot-figure-lines\">\n            {caption ? <span className=\"ot-figure-text\">{caption}</span> : null}\n            {credit ? <span className=\"ot-figure-credit\">{credit}</span> : null}\n          </Meta>\n        </figcaption>\n      ) : null}\n    </figure>\n  )\n}\n\nexport { Figure, type FigureProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/figure.css",
      "content": "@layer components {\n  /* ── ot-figure ── a picture cropped to a ratio and marked like a printer's proof (the aspect ratio's trim marks), then named in\n     type: its number as data, the caption in the voice, the credit spread to the far end on the same baseline, as \"the silence\n     that heals\" sets its camera and its archive under the picture. No filter, no mask, no zoom: the picture is shown as it is. */\n  .ot-figure { display: grid; gap: var(--ot-space-6); margin: 0; min-width: 0; container-type: inline-size; }\n  .ot-figure-frame { width: 100%; overflow: visible; }\n  .ot-figure-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }\n  /* The picture that isn't there is described in its place, inside the marks, the way a layout holds a place for one. */\n  .ot-figure-alt { display: none; max-width: 30ch; padding: var(--ot-space-4); text-align: center; font-size: var(--ot-pp); line-height: var(--ot-pp-lh); color: var(--ot-pencil); }\n  .ot-figure[data-missing] .ot-figure-img { opacity: 0; }\n  .ot-figure[data-missing] .ot-figure-alt { display: block; }\n\n  .ot-figure-caption { display: flex; align-items: first baseline; gap: var(--ot-space-4); min-width: 0; }\n  .ot-figure-number { flex: none; font-size: var(--ot-mp); line-height: 1; font-weight: 300; letter-spacing: var(--ot-mp-tr); font-variant-numeric: tabular-nums lining-nums; color: var(--ot-ink); }\n  .ot-figure-lines { flex: 1; min-width: 0; }\n  .ot-figure-text { max-width: 44ch; text-wrap: pretty; }\n  .ot-figure-credit { max-width: 24ch; color: var(--ot-pencil); text-align: end; }\n  /* Narrow: no room to spread the credit to the far end, so it steps under the caption and the hairline goes. */\n  @container (width < 26rem) {\n    .ot-figure-lines { flex-direction: column; align-items: start; gap: var(--ot-space-1); }\n    .ot-figure-lines > hr { display: none; }\n    .ot-figure-credit { text-align: start; }\n  }\n\n  /* margin: the caption stands in the margin beside the picture, as a book sets a side caption in the columns kept for notes (3 of 12):\n     the number at the head, level with the picture's top, the caption and credit at the foot, level with its foot.\n     Where the two can't stand side by side the caption falls under the picture. */\n  .ot-figure[data-variant=\"margin\"] { display: flex; flex-flow: row-reverse wrap; gap: var(--ot-space-5) var(--ot-space-6); }\n  .ot-figure[data-variant=\"margin\"] > .ot-figure-frame { flex: 9 1 0; min-width: min(100%, 20rem); }\n  .ot-figure[data-variant=\"margin\"] > .ot-figure-caption { flex: 3 1 0; min-width: 9rem; flex-direction: column; justify-content: space-between; align-items: stretch; gap: var(--ot-space-4); }\n  .ot-figure[data-variant=\"margin\"] .ot-figure-number { font-size: var(--ot-mf); letter-spacing: var(--ot-mf-tr); font-weight: 200; }\n  .ot-figure[data-variant=\"margin\"] .ot-figure-lines { flex: none; flex-direction: column; align-items: start; gap: var(--ot-space-2); }\n  .ot-figure[data-variant=\"margin\"] .ot-figure-lines > hr { display: none; }\n  .ot-figure[data-variant=\"margin\"] .ot-figure-credit { text-align: start; }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/figure.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/figure.css\"": {}
  },
  "meta": {
    "movement": "IX",
    "underneath": "native",
    "contract": "ot-figure"
  },
  "type": "registry:ui"
}
