{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aspect-ratio",
  "type": "registry:ui",
  "title": "Aspect Ratio",
  "description": "A ratio-box wrapper that locks a child (image, video, iframe, embed, or chart slot) to a fixed width-to-height ratio while it resizes fluidly, eliminating layout shift. Built on the native CSS aspect-ratio property plus object-fit — one CSS declaration, zero JavaScript.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/aspect-ratio.tsx",
      "type": "registry:ui",
      "target": "components/ui/aspect-ratio.tsx",
      "content": "/** @jsxImportSource hono/jsx */\nimport type { Child } from \"hono/jsx\"\nimport { cloneElement, isValidElement } from \"hono/jsx\"\nimport { cn, type ClassValue } from \"@/registry/lib/cn\"\n\n// Aspect Ratio — shadcn-htmx, htmx v4 + Tailwind v4.\n//\n// A ratio-box wrapper that locks a child (image / video / iframe / embed /\n// chart slot) to a fixed width-to-height ratio while it resizes fluidly,\n// eliminating layout shift. One CSS declaration does all the work — there\n// is no JavaScript here.\n//\n// shadcn/ui's upstream AspectRatio wraps Radix's primitive, which predates\n// native browser support and emulates the ratio with a padding-bottom hack\n// + absolute positioning. We do NOT copy that: the platform now ships the\n// real thing, so we use the native CSS `aspect-ratio` property instead. No\n// hacks (see AGENTS.md rule 4).\n//   Upstream (anatomy only):\n//     repos/shadcn-ui/apps/v4/registry/new-york-v4/ui/aspect-ratio.tsx\n//\n// Built on:\n//   - CSS `aspect-ratio` — defines the desired width-to-height ratio of the\n//     box; the browser keeps it as the box resizes. At least one of the\n//     box's sizes must be automatic for it to take effect (we leave height\n//     auto, width fluid).\n//       repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md\n//   - CSS `object-fit` — how a replaced element (img/video) fills the box:\n//     `cover` crops to fill, `contain` letterboxes to fit. Note object-fit\n//     has no effect on <iframe>/<embed>, which already stretch to the box.\n//       repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md\n//   - web.dev \"Aspect ratio image card\" pattern (`aspect-ratio: 16 / 9`,\n//     no padding-top hack):\n//       repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md\n//\n// Tailwind v4: `aspect-video` = 16/9, `aspect-square` = 1/1; any other\n// ratio is the arbitrary `aspect-[w/h]` utility. `object-cover` /\n// `object-contain` map to the object-fit keywords.\n\nexport type AspectRatioFit = \"cover\" | \"contain\"\n\n// Named ratios mapped to Tailwind's stock aspect utilities; everything else\n// falls through to the arbitrary `aspect-[w/h]` form below.\nconst NAMED_RATIO: Record<string, string> = {\n  \"1/1\": \"aspect-square\",\n  \"16/9\": \"aspect-video\",\n}\n\nconst fitClasses: Record<AspectRatioFit, string> = {\n  cover: \"object-cover\",\n  contain: \"object-contain\",\n}\n\n// Turn a ratio prop into a Tailwind class. Accepts:\n//   - a number   → 1.78        → aspect-[1.78]\n//   - \"16/9\"     → aspect-video (or aspect-[w/h] for unmapped ratios)\nfunction ratioClass(ratio: number | string): string {\n  if (typeof ratio === \"number\") return `aspect-[${ratio}]`\n  const key = ratio.replace(/\\s+/g, \"\")\n  return NAMED_RATIO[key] ?? `aspect-[${key}]`\n}\n\ntype AspectRatioProps = {\n  // Width-to-height ratio. A number (e.g. 1.778) or a \"w/h\" string\n  // (e.g. \"16/9\", \"4/3\"). Defaults to a 16:9 video frame.\n  ratio?: number | string\n  // How a replaced child (img/video) fills the box. `cover` crops, `contain`\n  // letterboxes. Ignored for non-replaced children (iframe/embed/div).\n  fit?: AspectRatioFit\n  class?: ClassValue\n  id?: string\n  // The locked element: an <img>, <video>, <iframe>, <embed>, or any block.\n  // A single valid element child is cloned so the sizing + object-fit\n  // classes land directly on it (the wrapper only carries the ratio).\n  children?: Child\n  // Forward hx-*, data-*, aria-*, and standard attributes onto the root.\n  [key: string]: unknown\n}\n\nconst root = \"relative block w-full overflow-hidden\"\n\nexport function AspectRatio(props: AspectRatioProps) {\n  const {\n    ratio = \"16/9\",\n    fit = \"cover\",\n    class: className,\n    id,\n    children,\n    ...rest\n  } = props\n\n  const rootClasses = cn(root, ratioClass(ratio), className)\n\n  // Annotate the single child so it stretches to fill the ratio box and\n  // applies object-fit (mirrors button/tooltip cloneElement convention).\n  let child: Child = children\n  if (isValidElement(children)) {\n    const el = children as any\n    child = cloneElement(el, {\n      \"data-slot\": \"aspect-ratio-content\",\n      class: cn(\"size-full\", fitClasses[fit], el?.props?.class),\n    })\n  }\n\n  return (\n    <div\n      id={id}\n      data-slot=\"aspect-ratio\"\n      data-ratio={typeof ratio === \"number\" ? String(ratio) : ratio}\n      class={rootClasses}\n      {...rest}\n    >\n      {child}\n    </div>\n  )\n}\n"
    },
    {
      "path": "registry/jinja2/aspect-ratio.html",
      "type": "registry:file",
      "target": "templates/components/aspect-ratio.html",
      "content": "{# Aspect Ratio macro — shadcn-htmx, htmx v4 + Tailwind v4.\n   Mirrors registry/ui/aspect-ratio.tsx.\n\n   Locks the slotted child to a fixed width-to-height ratio with the native\n   CSS `aspect-ratio` property (no padding-top hack) and `object-fit` for\n   replaced elements. Zero JS.\n     MDN aspect-ratio: repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md\n     MDN object-fit:   repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md\n     web.dev pattern:  repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md\n\n   The slotted child (passed via {{ caller() }}) should carry\n   `size-full object-cover` / `object-contain` itself so the fit lands on\n   the replaced element — mirroring how the .tsx clones the child.\n\n   Usage:\n     {% from \"components/aspect-ratio.html\" import aspect_ratio %}\n     {% call aspect_ratio(ratio=\"16/9\") %}\n       <img src=\"/photo.jpg\" alt=\"…\" class=\"size-full object-cover\">\n     {% endcall %} #}\n\n{% macro aspect_ratio(ratio=\"16/9\", id=none, extra_class=\"\", **attrs) %}\n{%- set ratio_class -%}\n{%- if ratio == \"1/1\" -%}aspect-square{%- elif ratio == \"16/9\" -%}aspect-video{%- else -%}aspect-[{{ ratio | replace(' ', '') }}]{%- endif -%}\n{%- endset -%}\n<div\n  {%- if id %} id=\"{{ id }}\"{% endif %}\n  data-slot=\"aspect-ratio\"\n  data-ratio=\"{{ ratio }}\"\n  class=\"relative block w-full overflow-hidden {{ ratio_class }} {{ extra_class }}\"\n  {%- for k, v in attrs.items() %} {{ k|replace('_','-') }}=\"{{ v }}\"{% endfor %}>{{ caller() }}</div>\n{% endmacro %}\n"
    },
    {
      "path": "registry/go-templates/aspect-ratio.tmpl",
      "type": "registry:file",
      "target": "components/aspect-ratio.tmpl",
      "content": "{{/*\n  Aspect Ratio template — shadcn-htmx, htmx v4 + Tailwind v4.\n  Mirrors registry/ui/aspect-ratio.tsx.\n\n  Locks the slotted child to a fixed width-to-height ratio with the native\n  CSS `aspect-ratio` property (no padding-top hack) and `object-fit` for\n  replaced elements. Zero JS.\n    MDN aspect-ratio: repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md\n    MDN object-fit:   repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md\n    web.dev pattern:  repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md\n\n      type AspectRatioArgs struct {\n          Ratio string      // \"16/9\" (default) | \"1/1\" | \"4/3\" | …\n          ID    string\n          Class string\n          Body  template.HTML // the slotted child, already carrying\n                              // size-full object-cover / object-contain\n      }\n*/}}\n\n{{define \"aspect-ratio\"}}\n{{- $ratio := or .Ratio \"16/9\" -}}\n{{- $ratioClass := printf \"aspect-[%s]\" $ratio -}}\n{{- if eq $ratio \"1/1\" -}}{{- $ratioClass = \"aspect-square\" -}}{{- else if eq $ratio \"16/9\" -}}{{- $ratioClass = \"aspect-video\" -}}{{- end -}}\n<div {{if .ID}}id=\"{{.ID}}\" {{end}}data-slot=\"aspect-ratio\" data-ratio=\"{{$ratio}}\" class=\"relative block w-full overflow-hidden {{$ratioClass}} {{.Class}}\">{{htmlSafe .Body}}</div>\n{{end}}\n"
    },
    {
      "path": "registry/phoenix/aspect_ratio.ex",
      "type": "registry:file",
      "target": "lib/my_app_web/components/aspect_ratio.ex",
      "content": "defmodule ShadcnHtmx.Components.AspectRatio do\n  @moduledoc \"\"\"\n  Aspect Ratio — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.\n\n  Mirrors registry/ui/aspect-ratio.tsx.\n\n  Locks the slotted child (image / video / iframe / embed / chart) to a\n  fixed width-to-height ratio with the native CSS `aspect-ratio` property\n  (no padding-top hack) and `object-fit` for replaced elements. Zero JS.\n\n    * MDN aspect-ratio:\n      repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md\n    * MDN object-fit:\n      repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md\n    * web.dev pattern:\n      repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md\n\n  The slotted child should carry `size-full object-cover` / `object-contain`\n  so the fit lands on the replaced element — mirroring how the .tsx clones\n  the child.\n\n  ## Examples\n\n      <.aspect_ratio ratio=\"16/9\">\n        <img src=\"/photo.jpg\" alt=\"…\" class=\"size-full object-cover\" />\n      </.aspect_ratio>\n\n      <.aspect_ratio ratio=\"1/1\">\n        <img src=\"/avatar.jpg\" alt=\"…\" class=\"size-full object-cover\" />\n      </.aspect_ratio>\n  \"\"\"\n\n  use Phoenix.Component\n\n  @root \"relative block w-full overflow-hidden\"\n\n  attr :ratio, :string, default: \"16/9\"\n  attr :class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block, required: true\n\n  def aspect_ratio(assigns) do\n    assigns =\n      assigns\n      |> assign(:root, @root)\n      |> assign(:ratio_class, ratio_class(assigns.ratio))\n\n    ~H\"\"\"\n    <div\n      data-slot=\"aspect-ratio\"\n      data-ratio={@ratio}\n      class={[@root, @ratio_class, @class]}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\n\n  defp ratio_class(\"1/1\"), do: \"aspect-square\"\n  defp ratio_class(\"16/9\"), do: \"aspect-video\"\n  defp ratio_class(ratio), do: \"aspect-[#{String.replace(ratio, \" \", \"\")}]\"\nend\n"
    },
    {
      "path": "registry/html/aspect-ratio.html",
      "type": "registry:file",
      "target": "snippets/aspect-ratio.html",
      "content": "<!--\n  shadcn-htmx — raw HTML aspect-ratio snippets.\n\n  Locks the slotted child to a fixed width-to-height ratio with the native\n  CSS `aspect-ratio` property (no padding-top hack). Replaced elements\n  (img/video) get `object-fit` via object-cover / object-contain; iframe /\n  embed already stretch to the box (object-fit has no effect on them).\n  Zero JavaScript — Tailwind utilities only.\n\n    MDN aspect-ratio: repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md\n    MDN object-fit:   repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md\n    web.dev pattern:  repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md\n\n  ROOT:\n    relative block w-full overflow-hidden  +  the ratio utility\n  RATIO utility:\n    1/1  → aspect-square\n    16/9 → aspect-video\n    any  → aspect-[w/h]   e.g. aspect-[4/3]\n-->\n\n<!-- 16:9 image, cropped to fill (object-cover) -->\n<div data-slot=\"aspect-ratio\" data-ratio=\"16/9\"\n     class=\"relative block w-full overflow-hidden aspect-video\">\n  <img src=\"/photo.jpg\" alt=\"Mountain lake at dawn\"\n       data-slot=\"aspect-ratio-content\"\n       class=\"size-full object-cover\">\n</div>\n\n<!-- 1:1 square, letterboxed to fit (object-contain) -->\n<div data-slot=\"aspect-ratio\" data-ratio=\"1/1\"\n     class=\"relative block w-full overflow-hidden aspect-square\">\n  <img src=\"/logo.png\" alt=\"Brand logo\"\n       data-slot=\"aspect-ratio-content\"\n       class=\"size-full object-contain\">\n</div>\n\n<!-- 16:9 responsive iframe (video embed) — object-fit has no effect here;\n     the iframe simply stretches to the ratio box. -->\n<div data-slot=\"aspect-ratio\" data-ratio=\"16/9\"\n     class=\"relative block w-full overflow-hidden aspect-video\">\n  <iframe src=\"https://www.youtube-nocookie.com/embed/VIDEO_ID\"\n          title=\"Embedded video\"\n          data-slot=\"aspect-ratio-content\"\n          class=\"size-full\" allowfullscreen></iframe>\n</div>\n\n<!-- Arbitrary 4:3 ratio with a plain content slot (e.g. a chart canvas) -->\n<div data-slot=\"aspect-ratio\" data-ratio=\"4/3\"\n     class=\"relative block w-full overflow-hidden aspect-[4/3]\">\n  <div data-slot=\"aspect-ratio-content\"\n       class=\"flex size-full items-center justify-center bg-muted text-muted-foreground\">\n    4 / 3 slot\n  </div>\n</div>\n"
    }
  ]
}
