{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aspect-ratio",
  "title": "Aspect ratio",
  "description": "A frame kept to a ratio and marked the way a printer marks a crop: short lines outside each corner, never a border.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/aspect-ratio.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { roll } from \"@/lib/0nlytype/roll\"\n\n/** 1.7778 to 16 : 9. Finds the smallest denominator up to 20 that lands on a whole number. */\nfunction toFraction(ratio: number): [number, number] {\n  for (let b = 1; b <= 20; b++) if (Math.abs(ratio * b - Math.round(ratio * b)) < 0.01) return [Math.round(ratio * b), b]\n  return [Math.round(ratio * 10), 10]\n}\n\ntype AspectRatioProps = React.ComponentProps<\"div\"> & {\n  /** Width over height. 16 / 9 by default. */\n  ratio?: number\n  /**\n   * What sits in the frame's middle. true draws the ratio as a fraction (16 over 9);\n   * it rolls when the ratio changes. Anything else is shown as given.\n   */\n  label?: React.ReactNode\n  /**\n   * crop: the printer's crop marks only.\n   * diagonal: the scaling diagonal, corner to corner, with the ratio written along it.\n   * square: the largest square ruled off, and what's left named by its own ratio.\n   */\n  variant?: \"crop\" | \"diagonal\" | \"square\"\n}\n\nconst gcd = (x: number, y: number): number => (y ? gcd(y, x % y) : x)\n\n/** A frame kept to a ratio and marked the way a printer marks a crop: short lines outside each corner, never a border. */\nfunction AspectRatio({ ratio = 16 / 9, label, variant = \"crop\", className, style, children, ...props }: AspectRatioProps) {\n  const [a, b] = toFraction(ratio)\n  const orient = a > b ? \"landscape\" : a < b ? \"portrait\" : \"square\"\n  // What's left once the square is ruled off: 16 : 9 leaves 7 : 9, 4 : 5 leaves 4 : 1.\n  const rest = orient === \"landscape\" ? [a - b, b] : [a, b - a]\n  const k = gcd(rest[0], rest[1]) || 1\n  const [shown, setShown] = React.useState<[number, number]>([a, b])\n  const before = React.useRef(ratio)\n  const refs = React.useRef<[HTMLSpanElement | null, HTMLSpanElement | null]>([null, null])\n\n  React.useEffect(() => {\n    if (before.current === ratio) return\n    const dir = ratio > before.current ? 1 : -1\n    before.current = ratio\n    const next = toFraction(ratio)\n    // The numerator and the denominator each roll the way the ratio moved.\n    refs.current.forEach((el, i) => el && roll(el, () => setShown((s) => (i ? [s[0], next[1]] : [next[0], s[1]])), \"0.5em\", dir))\n  }, [ratio])\n\n  return (\n    <div\n      data-slot=\"aspect-ratio\"\n      data-variant={variant}\n      data-orient={orient}\n      style={{ \"--ratio\": ratio, ...style } as React.CSSProperties}\n      className={cn(\"ot-ratio\", className)}\n      {...props}\n    >\n      {label === true ? (\n        <span className=\"ot-fraction\" aria-hidden=\"true\">\n          <span ref={(el) => void (refs.current[0] = el)} className=\"ot-yours\">{shown[0]}</span>\n          <i />\n          <span ref={(el) => void (refs.current[1] = el)}>{shown[1]}</span>\n        </span>\n      ) : null}\n      {label === true ? (\n        <span className=\"ot-sr\">\n          {a} by {b}\n        </span>\n      ) : (\n        label\n      )}\n      {label === true && variant === \"square\" && orient !== \"square\" ? (\n        // Keyed, so the name of what's left arrives again once the square has glided to its new place.\n        <span key={`${orient} ${rest}`} className=\"ot-ratio-rest\" aria-hidden=\"true\">\n          <span className=\"ot-fraction\">\n            <span>{rest[0] / k}</span>\n            <i />\n            <span>{rest[1] / k}</span>\n          </span>\n        </span>\n      ) : null}\n      {children}\n    </div>\n  )\n}\n\nexport { AspectRatio, type AspectRatioProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/aspect-ratio.css",
      "content": "@layer components {\n  /* ── ot-ratio ── a frame kept to a ratio, marked the way a printer marks a crop: trim marks, never a border */\n  @property --ratio { syntax: \"<number>\"; inherits: false; initial-value: 1.7778; }\n  .ot-ratio {\n    /* --ratio is registered so it can ease, and so it doesn't inherit; --r carries it to the parts and pseudo-elements. */\n    --r: var(--ratio); --c: var(--ot-ink); --line: var(--ot-rule-strong);\n    position: relative; display: grid; place-items: center; aspect-ratio: var(--ratio); container-type: inline-size;\n    transition: --ratio var(--ot-andante) var(--ot-breath);\n  }\n  .ot-ratio::before {\n    --m: 12px; --g: 6px; --e: calc(var(--m) + var(--g));\n    content: \"\"; position: absolute; inset: calc(-1 * var(--e)); pointer-events: none;\n    background:\n      linear-gradient(var(--c), var(--c)) 0 var(--e) / var(--m) var(--ot-hairline),\n      linear-gradient(var(--c), var(--c)) 100% var(--e) / var(--m) var(--ot-hairline),\n      linear-gradient(var(--c), var(--c)) 0 calc(100% - var(--e)) / var(--m) var(--ot-hairline),\n      linear-gradient(var(--c), var(--c)) 100% calc(100% - var(--e)) / var(--m) var(--ot-hairline),\n      linear-gradient(var(--c), var(--c)) var(--e) 0 / var(--ot-hairline) var(--m),\n      linear-gradient(var(--c), var(--c)) calc(100% - var(--e)) 0 / var(--ot-hairline) var(--m),\n      linear-gradient(var(--c), var(--c)) var(--e) 100% / var(--ot-hairline) var(--m),\n      linear-gradient(var(--c), var(--c)) calc(100% - var(--e)) 100% / var(--ot-hairline) var(--m);\n    background-repeat: no-repeat;\n  }\n  .ot-ratio > .ot-fraction > *, .ot-ratio-rest .ot-fraction > * { display: inline-block; }\n  /* The fraction keeps to the frame: full size in a wide one, smaller in a small one. */\n  .ot-ratio > .ot-fraction { font-size: min(var(--ot-f), 17cqi); }\n\n  /* Diagonal: the paste-up artist's scaling line, from the foot of the start edge to the head of the end edge.\n     Every frame whose corner lies on it has the same ratio. The ratio is written along it, standing on it,\n     and swings with it as the frame eases. */\n  .ot-ratio[data-variant=\"diagonal\"]::after {\n    content: \"\"; position: absolute; inset: 0; pointer-events: none;\n    background: linear-gradient(to bottom right, transparent calc(50% - 0.6px), var(--line) calc(50% - 0.6px) calc(50% + 0.6px), transparent calc(50% + 0.6px));\n  }\n  .ot-ratio[data-variant=\"diagonal\"] > .ot-fraction { transform: rotate(calc(-1 * atan2(1, var(--r)))) translateY(-62%); }\n  .ot-ratio[data-variant=\"diagonal\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *)::after { background: linear-gradient(to bottom left, transparent calc(50% - 0.6px), var(--line) calc(50% - 0.6px) calc(50% + 0.6px), transparent calc(50% + 0.6px)); }\n  .ot-ratio[data-variant=\"diagonal\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) > .ot-fraction { transform: rotate(atan2(1, var(--r))) translateY(-62%); }\n\n  /* Square: the largest square ruled off from the start (or the head, when the frame stands tall),\n     the ratio in the square and what's left named by its own, in the pencil. The rule glides as the frame eases;\n     at 1 : 1 there is nothing left and it runs to nothing. */\n  .ot-ratio[data-variant=\"square\"]::after {\n    content: \"\"; position: absolute; inset: 0; pointer-events: none;\n    background:\n      linear-gradient(var(--line), var(--line)) no-repeat calc(100% / var(--r)) 0 / var(--ot-hairline) calc(clamp(0, (var(--r) - 1) * 1000, 1) * 100%),\n      linear-gradient(var(--line), var(--line)) no-repeat 0 calc(var(--r) * 100%) / calc(clamp(0, (1 - var(--r)) * 1000, 1) * 100%) var(--ot-hairline);\n  }\n  .ot-ratio[data-variant=\"square\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *)::after { background-position: calc(100% - 100% / var(--r)) 0, 0 calc(var(--r) * 100%); }\n  .ot-ratio[data-variant=\"square\"] > .ot-fraction { position: absolute; display: flex; flex-wrap: wrap; place-content: center; }\n  .ot-ratio-rest { position: absolute; display: grid; place-items: center; animation: ot-ratio-rest var(--ot-moderato) var(--ot-exhale) var(--ot-andante) backwards; }\n  @keyframes ot-ratio-rest { from { opacity: 0; } }\n  .ot-ratio[data-variant=\"square\"][data-orient=\"landscape\"] > .ot-fraction { inset-block: 0; inset-inline-start: 0; width: calc(100% / var(--r)); }\n  .ot-ratio[data-variant=\"square\"][data-orient=\"portrait\"] > .ot-fraction { inset-inline: 0; inset-block-start: 0; height: calc(var(--r) * 100%); }\n  .ot-ratio-rest .ot-fraction { font-size: min(var(--ot-mp), 9cqi); }\n  .ot-ratio-rest .ot-fraction > * { color: var(--ot-pencil); }\n  .ot-ratio[data-orient=\"landscape\"] > .ot-ratio-rest { inset-block: 0; inset-inline: calc(100% / var(--r)) 0; }\n  .ot-ratio[data-orient=\"portrait\"] > .ot-ratio-rest { inset-inline: 0; inset-block: calc(var(--r) * 100%) 0; }\n\n  /* Forced colours would drop the marks and lines, which are background images: keep them, in CanvasText. */\n  @media (forced-colors: active) { .ot-ratio { --c: CanvasText; --line: CanvasText; } .ot-ratio::before, .ot-ratio::after { forced-color-adjust: none; } }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/aspect-ratio.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/aspect-ratio.css\"": {}
  },
  "meta": {
    "movement": "IX",
    "underneath": "native",
    "contract": "ot-ratio"
  },
  "type": "registry:ui"
}
