{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cascading-select",
  "type": "registry:ui",
  "title": "Cascading Select",
  "description": "Two dependent native <select>s: choosing the parent reloads the child's <option>s — and an optional detail panel — from the server via an hx-swap-oob fragment. Cascade rides htmx's default `change` trigger on <select>; zero custom JS.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/cascading-select.tsx",
      "type": "registry:ui",
      "target": "components/ui/cascading-select.tsx",
      "content": "/** @jsxImportSource hono/jsx */\nimport type { Child, PropsWithChildren } from \"hono/jsx\"\nimport { cn, type ClassValue } from \"@/registry/lib/cn\"\n\n// Cascading Select — shadcn-htmx, htmx v4 + Tailwind v4.\n//\n// A pair of dependent native <select>s: picking the parent (e.g. car make)\n// reloads the child's <option>s (e.g. model) — and, optionally, a detail\n// panel — from the server. One request, two updates: the response swaps the\n// child options into the target, and a second fragment carrying\n// hx-swap-oob updates the detail panel \"out of band\".\n//\n// Built on:\n//   repos/htmx/www/src/content/patterns/02-forms/04-linked-selects.md\n//     The canonical linked-selects recipe: parent <select hx-get hx-target>\n//     swaps a fresh <option> list into the child; a detail card rides along.\n//   repos/htmx/www/src/content/reference/01-attributes/06-hx-trigger.md:32-37\n//     htmx defaults the trigger to `change` for <select>, so NO hx-trigger is\n//     needed — choosing an option fires the request.\n//   repos/htmx/www/src/content/reference/01-attributes/13-hx-swap-oob.md\n//     hx-swap-oob=\"true\" on the detail fragment swaps it into #<id>-detail by\n//     id, piggybacking a second update onto the same response.\n//   repos/htmx/www/src/content/reference/01-attributes/07-hx-swap.md\n//     default swap is innerHTML — the returned <option>s replace the child's\n//     contents; hx-include carries the parent value with the request.\n//\n// Native semantics (the whole control is two real <select>s in a <fieldset>):\n//   repos/mdn/files/en-us/web/html/reference/elements/select/index.md\n//   repos/mdn/files/en-us/web/html/reference/elements/option/index.md\n//   repos/mdn/files/en-us/web/html/reference/elements/fieldset/index.md\n//     The <fieldset> + <legend> groups the related controls for AT. Each\n//     <select> brings keyboard control, type-to-search, mobile pickers, and\n//     form submission with zero JS. With htmx off the parent still submits\n//     its value in a normal form post — progressive enhancement, not emulation.\n//\n// JS budget: NONE. The cascade is htmx's default `change` trigger + an OOB\n// swap; there is no site.js for this component.\n//\n// Style analogues: registry/ui/select.tsx (the chevron-overlaid native\n// <select>, classes mirrored verbatim) and registry/ui/edit-in-place.tsx\n// (server-fragment composite returning bare <option>/<div> partials).\n\n// Mirrors registry/ui/select.tsx `triggerBase` verbatim so a cascading\n// select is visually identical to a standalone Select.\nconst triggerBase =\n  \"peer flex h-9 w-full min-w-0 cursor-pointer appearance-none items-center rounded-md border border-input bg-background px-3 pr-8 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none \" +\n  \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 \" +\n  \"disabled:cursor-not-allowed disabled:opacity-50 \" +\n  \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 \" +\n  \"md:text-sm dark:bg-input/30 \" +\n  \"[&.htmx-request]:opacity-70\"\n\nconst fieldsetClass = \"grid gap-4\"\nconst legendClass =\n  \"mb-1 text-sm leading-none font-medium text-foreground\"\nconst fieldClass = \"grid gap-2\"\nconst fieldLabelClass =\n  \"text-sm leading-none font-medium text-foreground select-none\"\nconst chevronClass =\n  \"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50\"\nconst detailClass = \"text-sm text-muted-foreground\"\n\nfunction Chevron() {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      class={chevronClass}\n      aria-hidden=\"true\"\n    >\n      <polyline points=\"6 9 12 15 18 9\" />\n    </svg>\n  )\n}\n\nexport type CascadingSelectProps = PropsWithChildren<{\n  // Base id. The child select is `${id}-child`; the detail panel (if used) is\n  // `${id}-detail`; the legend is `${id}-legend`.\n  id: string\n  // Endpoint the PARENT requests on change. Returns the child's <option>s,\n  // and (optionally) a detail fragment with hx-swap-oob=\"true\".\n  endpoint: string\n  // Form field names. Defaults: parent \"parent\", child \"child\".\n  parentName?: string\n  childName?: string\n  // Visible group label rendered as <legend>.\n  legend?: string\n  // Per-select field labels (rendered as <label for>).\n  parentLabel?: string\n  childLabel?: string\n  // Parent <option>s. Pass <option> elements (or SelectOption).\n  children: Child\n  // Initial child <option>s, shown before the first change fires.\n  childOptions?: Child\n  // Initial detail-panel content. Omit to skip the detail panel entirely.\n  detail?: Child\n  // Disable the whole group.\n  disabled?: boolean\n  // GET keeps the cascade idempotent + the no-JS form post shareable.\n  method?: \"get\" | \"post\"\n  class?: ClassValue\n  // Escape hatch: forward arbitrary hx-* / data-* / aria-* onto the parent\n  // <select> (e.g. hx-indicator). Overrides the defaults below.\n  [key: `hx-${string}`]: any\n  [key: `data-${string}`]: any\n  [key: `aria-${string}`]: any\n}>\n\nexport function CascadingSelect(props: CascadingSelectProps) {\n  const {\n    id,\n    endpoint,\n    parentName = \"parent\",\n    childName = \"child\",\n    legend,\n    parentLabel,\n    childLabel,\n    children,\n    childOptions,\n    detail,\n    disabled,\n    method = \"get\",\n    class: className,\n    ...rest\n  } = props\n\n  const childId = `${id}-child`\n  const detailId = `${id}-detail`\n  const legendId = `${id}-legend`\n\n  // The parent's request: GET the endpoint, swap the child's <option>s\n  // (innerHTML, the default). hx-include pins the parent value to the request\n  // by name even if the trigger element changes. A detail fragment in the\n  // response carries hx-swap-oob=\"true\" to update #${id}-detail too.\n  // No hx-trigger: htmx defaults <select> to `change`.\n  const hxKey = method === \"post\" ? \"hx-post\" : \"hx-get\"\n  const parentHx: Record<string, any> = {\n    [hxKey]: endpoint,\n    \"hx-target\": `#${childId}`,\n    \"hx-include\": `[name='${parentName}']`,\n  }\n  const hx = { ...parentHx, ...rest }\n\n  return (\n    <fieldset\n      data-slot=\"cascading-select\"\n      id={id}\n      disabled={disabled}\n      class={cn(fieldsetClass, className)}\n      aria-labelledby={legend ? legendId : undefined}\n    >\n      {legend ? (\n        <legend id={legendId} class={legendClass} data-slot=\"cascading-select-legend\">\n          {legend}\n        </legend>\n      ) : null}\n\n      <div class={fieldClass}>\n        {parentLabel ? (\n          <label for={`${id}-parent`} class={fieldLabelClass}>\n            {parentLabel}\n          </label>\n        ) : null}\n        <span class=\"relative inline-flex w-full\">\n          <select\n            id={`${id}-parent`}\n            name={parentName}\n            data-slot=\"cascading-select-parent\"\n            class={triggerBase}\n            aria-controls={detail !== undefined ? `${childId} ${detailId}` : childId}\n            {...hx}\n          >\n            {children}\n          </select>\n          <Chevron />\n        </span>\n      </div>\n\n      <div class={fieldClass}>\n        {childLabel ? (\n          <label for={childId} class={fieldLabelClass}>\n            {childLabel}\n          </label>\n        ) : null}\n        <span class=\"relative inline-flex w-full\">\n          {/* The cascade target. htmx swaps fresh <option>s in here. */}\n          <select\n            id={childId}\n            name={childName}\n            data-slot=\"cascading-select-child\"\n            class={triggerBase}\n          >\n            {childOptions}\n          </select>\n          <Chevron />\n        </span>\n      </div>\n\n      {detail !== undefined ? (\n        // OOB swap target. aria-live so AT announces the detail change that\n        // accompanies the option swap.\n        <div\n          id={detailId}\n          data-slot=\"cascading-select-detail\"\n          aria-live=\"polite\"\n          class={detailClass}\n        >\n          {detail}\n        </div>\n      ) : null}\n    </fieldset>\n  )\n}\n\n// Re-export the native primitive for ergonomic option authoring (mirrors\n// registry/ui/select.tsx). <option> needs no styling beyond the browser's.\nexport function CascadingSelectOption(\n  props: PropsWithChildren<{ value: string; selected?: boolean; disabled?: boolean }>,\n) {\n  const { value, selected, disabled, children } = props\n  return (\n    <option value={value} selected={selected} disabled={disabled}>\n      {children}\n    </option>\n  )\n}\n\n// A detail fragment ready for an out-of-band swap. Return this from the\n// endpoint alongside the bare <option>s; htmx swaps it into #${id}-detail.\n//\n// hx-swap-oob=\"innerHTML\" (not \"true\"/outerHTML) so the live #${id}-detail\n// element stays in the DOM — only its contents are replaced. An outerHTML OOB\n// swap would detach and replace the node, breaking the aria-live region's\n// identity (and any element reference held to it). The encapsulating <div> is\n// stripped by htmx; its children are swapped into the element matched by id.\nexport function CascadingSelectDetail(\n  props: PropsWithChildren<{ id: string }>,\n) {\n  return (\n    <div\n      id={`${props.id}-detail`}\n      data-slot=\"cascading-select-detail\"\n      hx-swap-oob=\"innerHTML\"\n      aria-live=\"polite\"\n      class={detailClass}\n    >\n      {props.children}\n    </div>\n  )\n}\n"
    },
    {
      "path": "registry/jinja2/cascading-select.html",
      "type": "registry:file",
      "target": "templates/components/cascading-select.html",
      "content": "{# Cascading Select macros — shadcn-htmx, htmx v4 + Tailwind v4.\n   Mirrors registry/ui/cascading-select.tsx. A pair of dependent native\n   <select>s: picking the parent reloads the child's <option>s (and an\n   optional detail panel via hx-swap-oob) from the server.\n\n   No hx-trigger: htmx defaults <select> to `change`.\n   See repos/htmx/www/src/content/patterns/02-forms/04-linked-selects.md\n\n   Usage (open … parent <option>s … close, like select.html):\n     {% from \"components/cascading-select.html\" import cascading_select_open, cascading_select_close, option, cascading_detail %}\n\n     {{ cascading_select_open(id=\"vehicle\", endpoint=\"/models\",\n            parent_name=\"make\", child_name=\"model\",\n            legend=\"Vehicle\", parent_label=\"Make\", child_label=\"Model\") }}\n       {{ option(\"audi\",   \"Audi\", selected=true) }}\n       {{ option(\"toyota\", \"Toyota\") }}\n     {{ cascading_select_close(id=\"vehicle\", child_name=\"model\", child_label=\"Model\") }}\n\n   The endpoint returns the child <option>s + the OOB detail fragment:\n     {{ option(\"a4\", \"A4\", selected=true) }} …\n     {% call cascading_detail(id=\"vehicle\") %}Audi A4 — Sedan, $39,900{% endcall %} #}\n\n{% macro cascading_select_open(\n    id,\n    endpoint,\n    parent_name=\"parent\",\n    child_name=\"child\",\n    legend=none,\n    parent_label=none,\n    child_label=none,\n    detail=true,\n    disabled=false,\n    method=\"get\",\n    extra_class=\"\",\n    **attrs\n) -%}\n{%- set base -%}\npeer flex h-9 w-full min-w-0 cursor-pointer appearance-none items-center rounded-md border border-input bg-background px-3 pr-8 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&.htmx-request]:opacity-70\n{%- endset -%}\n{%- set hx_attr = \"hx-post\" if method == \"post\" else \"hx-get\" -%}\n<fieldset data-slot=\"cascading-select\" id=\"{{ id }}\" class=\"grid gap-4 {{ extra_class }}\"\n          {%- if disabled %} disabled{% endif %}\n          {%- if legend %} aria-labelledby=\"{{ id }}-legend\"{% endif -%}\n>\n  {%- if legend %}\n  <legend id=\"{{ id }}-legend\" class=\"mb-1 text-sm leading-none font-medium text-foreground\" data-slot=\"cascading-select-legend\">{{ legend }}</legend>\n  {%- endif %}\n  <div class=\"grid gap-2\">\n    {%- if parent_label %}\n    <label for=\"{{ id }}-parent\" class=\"text-sm leading-none font-medium text-foreground select-none\">{{ parent_label }}</label>\n    {%- endif %}\n    <span class=\"relative inline-flex w-full\">\n      <select id=\"{{ id }}-parent\" name=\"{{ parent_name }}\" data-slot=\"cascading-select-parent\"\n              class=\"{{ base }}\"\n              {{ hx_attr }}=\"{{ endpoint }}\"\n              hx-target=\"#{{ id }}-child\"\n              hx-include=\"[name='{{ parent_name }}']\"\n              aria-controls=\"{% if detail %}{{ id }}-child {{ id }}-detail{% else %}{{ id }}-child{% endif %}\"\n              {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}=\"{{ v }}\"{% endfor -%}\n      >\n{%- endmacro %}\n\n{% macro cascading_select_close(\n    id,\n    child_name=\"child\",\n    child_label=none,\n    detail=true,\n    child_body=\"\"\n) -%}\n      </select>\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n           class=\"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50\" aria-hidden=\"true\">\n        <polyline points=\"6 9 12 15 18 9\" />\n      </svg>\n    </span>\n  </div>\n  <div class=\"grid gap-2\">\n    {%- if child_label %}\n    <label for=\"{{ id }}-child\" class=\"text-sm leading-none font-medium text-foreground select-none\">{{ child_label }}</label>\n    {%- endif %}\n    <span class=\"relative inline-flex w-full\">\n      <select id=\"{{ id }}-child\" name=\"{{ child_name }}\" data-slot=\"cascading-select-child\"\n              class=\"peer flex h-9 w-full min-w-0 cursor-pointer appearance-none items-center rounded-md border border-input bg-background px-3 pr-8 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&.htmx-request]:opacity-70\">{{ child_body|safe }}</select>\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n           class=\"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50\" aria-hidden=\"true\">\n        <polyline points=\"6 9 12 15 18 9\" />\n      </svg>\n    </span>\n  </div>\n  {%- if detail %}\n  <div id=\"{{ id }}-detail\" data-slot=\"cascading-select-detail\" aria-live=\"polite\" class=\"text-sm text-muted-foreground\"></div>\n  {%- endif %}\n</fieldset>\n{%- endmacro %}\n\n{% macro option(value, text, selected=false, disabled=false) -%}\n<option value=\"{{ value }}\"\n        {%- if selected %} selected{% endif %}\n        {%- if disabled %} disabled{% endif -%}\n>{{ text }}</option>\n{%- endmacro %}\n\n{# Detail fragment for the OOB swap — return alongside the child <option>s.\n   hx-swap-oob=\"innerHTML\" (not \"true\"/outerHTML) keeps the live #id-detail node\n   in the DOM and swaps only its contents, preserving the aria-live region's\n   identity across updates. #}\n{% macro cascading_detail(id) -%}\n<div id=\"{{ id }}-detail\" data-slot=\"cascading-select-detail\" hx-swap-oob=\"innerHTML\" aria-live=\"polite\" class=\"text-sm text-muted-foreground\">{{ caller() }}</div>\n{%- endmacro %}\n"
    },
    {
      "path": "registry/go-templates/cascading-select.tmpl",
      "type": "registry:file",
      "target": "components/cascading-select.tmpl",
      "content": "{{/*\n  Cascading Select template — shadcn-htmx, htmx v4 + Tailwind v4.\n  Mirrors registry/ui/cascading-select.tsx. A pair of dependent native\n  <select>s: picking the parent reloads the child's <option>s (and an\n  optional detail panel via hx-swap-oob) from the server.\n\n  No hx-trigger: htmx defaults <select> to `change`.\n  See repos/htmx/www/src/content/patterns/02-forms/04-linked-selects.md\n\n  Usage:\n\n      type CascadingSelectArgs struct {\n          ID, Endpoint              string\n          ParentName, ChildName     string // default \"parent\" / \"child\"\n          Legend                    string\n          ParentLabel, ChildLabel   string\n          Method                    string // \"get\" (default) | \"post\"\n          Disabled                  bool\n          Detail                    bool   // render the OOB detail panel\n          ExtraClass                string\n          // PARENT <option>s, pre-rendered.\n          Body template.HTML\n          // Initial CHILD <option>s for the selected parent, pre-rendered.\n          ChildBody template.HTML\n          Attrs map[string]string\n      }\n*/}}\n\n{{define \"cascading-select\"}}\n{{- $base := \"peer flex h-9 w-full min-w-0 cursor-pointer appearance-none items-center rounded-md border border-input bg-background px-3 pr-8 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&.htmx-request]:opacity-70\" -}}\n{{- $parentName := or .ParentName \"parent\" -}}\n{{- $childName := or .ChildName \"child\" -}}\n{{- $hx := \"hx-get\" -}}{{- if eq .Method \"post\" }}{{- $hx = \"hx-post\" -}}{{- end -}}\n<fieldset data-slot=\"cascading-select\" id=\"{{.ID}}\" class=\"grid gap-4 {{.ExtraClass}}\"\n          {{- if .Disabled}} disabled{{end}}\n          {{- if .Legend}} aria-labelledby=\"{{.ID}}-legend\"{{end -}}\n>\n  {{- if .Legend}}\n  <legend id=\"{{.ID}}-legend\" class=\"mb-1 text-sm leading-none font-medium text-foreground\" data-slot=\"cascading-select-legend\">{{.Legend}}</legend>\n  {{- end}}\n  <div class=\"grid gap-2\">\n    {{- if .ParentLabel}}\n    <label for=\"{{.ID}}-parent\" class=\"text-sm leading-none font-medium text-foreground select-none\">{{.ParentLabel}}</label>\n    {{- end}}\n    <span class=\"relative inline-flex w-full\">\n      <select id=\"{{.ID}}-parent\" name=\"{{$parentName}}\" data-slot=\"cascading-select-parent\"\n              class=\"{{$base}}\"\n              {{$hx}}=\"{{.Endpoint}}\"\n              hx-target=\"#{{.ID}}-child\"\n              hx-include=\"[name='{{$parentName}}']\"\n              aria-controls=\"{{if .Detail}}{{.ID}}-child {{.ID}}-detail{{else}}{{.ID}}-child{{end}}\"\n              {{- range $k, $v := .Attrs}} {{$k}}=\"{{$v}}\"{{end -}}\n      >{{.Body}}</select>\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n           class=\"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50\" aria-hidden=\"true\">\n        <polyline points=\"6 9 12 15 18 9\" />\n      </svg>\n    </span>\n  </div>\n  <div class=\"grid gap-2\">\n    {{- if .ChildLabel}}\n    <label for=\"{{.ID}}-child\" class=\"text-sm leading-none font-medium text-foreground select-none\">{{.ChildLabel}}</label>\n    {{- end}}\n    <span class=\"relative inline-flex w-full\">\n      <select id=\"{{.ID}}-child\" name=\"{{$childName}}\" data-slot=\"cascading-select-child\" class=\"{{$base}}\">{{.ChildBody}}</select>\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n           class=\"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50\" aria-hidden=\"true\">\n        <polyline points=\"6 9 12 15 18 9\" />\n      </svg>\n    </span>\n  </div>\n  {{- if .Detail}}\n  <div id=\"{{.ID}}-detail\" data-slot=\"cascading-select-detail\" aria-live=\"polite\" class=\"text-sm text-muted-foreground\"></div>\n  {{- end}}\n</fieldset>\n{{end}}\n\n{{/* Detail fragment for the OOB swap — return alongside the child <option>s.\n     hx-swap-oob=\"innerHTML\" (not \"true\"/outerHTML) keeps the live #ID-detail\n     node in the DOM and swaps only its contents, preserving the aria-live\n     region's identity across updates.\n     Args: (dict \"ID\" \"vehicle\" \"Body\" (htmlSafe \"…\")) */}}\n{{define \"cascading-select-detail\"}}\n<div id=\"{{.ID}}-detail\" data-slot=\"cascading-select-detail\" hx-swap-oob=\"innerHTML\" aria-live=\"polite\" class=\"text-sm text-muted-foreground\">{{.Body}}</div>\n{{end}}\n"
    },
    {
      "path": "registry/phoenix/cascading_select.ex",
      "type": "registry:file",
      "target": "lib/my_app_web/components/cascading_select.ex",
      "content": "defmodule ShadcnHtmx.Components.CascadingSelect do\n  @moduledoc \"\"\"\n  Cascading Select — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.\n\n  Mirrors registry/ui/cascading-select.tsx. A pair of dependent native\n  `<select>`s: picking the parent reloads the child's `<option>`s (and an\n  optional detail panel via `hx-swap-oob`) from the server.\n\n  No `hx-trigger`: htmx defaults `<select>` to `change`.\n  See repos/htmx/www/src/content/patterns/02-forms/04-linked-selects.md\n\n  ## Examples\n\n      <.cascading_select id=\"vehicle\" endpoint={~p\"/models\"}\n        parent_name=\"make\" child_name=\"model\"\n        legend=\"Vehicle\" parent_label=\"Make\" child_label=\"Model\">\n        <option value=\"audi\" selected>Audi</option>\n        <option value=\"toyota\">Toyota</option>\n      </.cascading_select>\n\n  The inner block provides the PARENT `<option>`s; pass the initially-selected\n  parent's options via the optional `child_options` slot so the child renders\n  populated before the first change. Return the child `<option>`s plus a\n  `<.cascading_select_detail>` carrying `hx-swap-oob` from the endpoint.\n  \"\"\"\n\n  use Phoenix.Component\n\n  @base \"peer flex h-9 w-full min-w-0 cursor-pointer appearance-none items-center rounded-md border border-input bg-background px-3 pr-8 py-1 text-base shadow-xs \" <>\n          \"transition-[color,box-shadow] outline-none \" <>\n          \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 \" <>\n          \"disabled:cursor-not-allowed disabled:opacity-50 \" <>\n          \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 \" <>\n          \"md:text-sm dark:bg-input/30 \" <>\n          \"[&.htmx-request]:opacity-70\"\n\n  attr :id, :string, required: true\n  attr :endpoint, :string, required: true\n  attr :parent_name, :string, default: \"parent\"\n  attr :child_name, :string, default: \"child\"\n  attr :legend, :string, default: nil\n  attr :parent_label, :string, default: nil\n  attr :child_label, :string, default: nil\n  attr :method, :string, default: \"get\", values: ~w(get post)\n  attr :disabled, :boolean, default: false\n  attr :detail, :boolean, default: true\n  attr :class, :string, default: nil\n\n  attr :rest, :global,\n    include: ~w(hx-indicator hx-swap hx-vals hx-sync hx-confirm hx-disabled-elt)\n\n  slot :inner_block, required: true\n  slot :child_options\n\n  def cascading_select(assigns) do\n    assigns =\n      assigns\n      |> assign(:base, @base)\n      |> assign(:hx_attr, if(assigns.method == \"post\", do: \"hx-post\", else: \"hx-get\"))\n      |> assign(\n        :controls,\n        if(assigns.detail,\n          do: \"#{assigns.id}-child #{assigns.id}-detail\",\n          else: \"#{assigns.id}-child\"\n        )\n      )\n\n    ~H\"\"\"\n    <fieldset\n      data-slot=\"cascading-select\"\n      id={@id}\n      disabled={@disabled}\n      class={[\"grid gap-4\", @class]}\n      aria-labelledby={@legend && \"#{@id}-legend\"}\n    >\n      <legend\n        :if={@legend}\n        id={\"#{@id}-legend\"}\n        class=\"mb-1 text-sm leading-none font-medium text-foreground\"\n        data-slot=\"cascading-select-legend\"\n      >\n        {@legend}\n      </legend>\n      <div class=\"grid gap-2\">\n        <label\n          :if={@parent_label}\n          for={\"#{@id}-parent\"}\n          class=\"text-sm leading-none font-medium text-foreground select-none\"\n        >\n          {@parent_label}\n        </label>\n        <span class=\"relative inline-flex w-full\">\n          <select\n            id={\"#{@id}-parent\"}\n            name={@parent_name}\n            data-slot=\"cascading-select-parent\"\n            class={@base}\n            {%{@hx_attr => @endpoint}}\n            hx-target={\"##{@id}-child\"}\n            hx-include={\"[name='#{@parent_name}']\"}\n            aria-controls={@controls}\n            {@rest}\n          >\n            {render_slot(@inner_block)}\n          </select>\n          <.chevron />\n        </span>\n      </div>\n      <div class=\"grid gap-2\">\n        <label\n          :if={@child_label}\n          for={\"#{@id}-child\"}\n          class=\"text-sm leading-none font-medium text-foreground select-none\"\n        >\n          {@child_label}\n        </label>\n        <span class=\"relative inline-flex w-full\">\n          <select\n            id={\"#{@id}-child\"}\n            name={@child_name}\n            data-slot=\"cascading-select-child\"\n            class={@base}\n          >\n            {render_slot(@child_options)}\n          </select>\n          <.chevron />\n        </span>\n      </div>\n      <div\n        :if={@detail}\n        id={\"#{@id}-detail\"}\n        data-slot=\"cascading-select-detail\"\n        aria-live=\"polite\"\n        class=\"text-sm text-muted-foreground\"\n      >\n      </div>\n    </fieldset>\n    \"\"\"\n  end\n\n  attr :id, :string, required: true\n  slot :inner_block, required: true\n\n  @doc \"\"\"\n  Detail fragment for the OOB swap — return alongside the child options.\n\n  `hx-swap-oob=\"innerHTML\"` (not `\"true\"`/`outerHTML`) keeps the live\n  `#id-detail` node in the DOM and swaps only its contents, preserving the\n  aria-live region's identity across updates.\n  \"\"\"\n  def cascading_select_detail(assigns) do\n    ~H\"\"\"\n    <div\n      id={\"#{@id}-detail\"}\n      data-slot=\"cascading-select-detail\"\n      hx-swap-oob=\"innerHTML\"\n      aria-live=\"polite\"\n      class=\"text-sm text-muted-foreground\"\n    >\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\n\n  defp chevron(assigns) do\n    ~H\"\"\"\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      class=\"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50\"\n      aria-hidden=\"true\"\n    >\n      <polyline points=\"6 9 12 15 18 9\" />\n    </svg>\n    \"\"\"\n  end\nend\n"
    },
    {
      "path": "registry/html/cascading-select.html",
      "type": "registry:file",
      "target": "snippets/cascading-select.html",
      "content": "<!--\n  shadcn-htmx — raw HTML cascading-select snippet.\n\n  Mirrors registry/ui/cascading-select.tsx. A pair of dependent native\n  <select>s in a <fieldset>: picking the parent reloads the child's\n  <option>s (and an optional detail panel via hx-swap-oob) from the server.\n\n  No hx-trigger: htmx defaults <select> to `change`.\n  See repos/htmx/www/src/content/patterns/02-forms/04-linked-selects.md\n\n  WIRING:\n    - Parent <select> hx-get=\"/models\" hx-target=\"#vehicle-child\"\n      hx-include=\"[name='make']\" — GETs the endpoint on change, swaps the\n      returned <option>s into the child (innerHTML, the default).\n    - The endpoint also returns a <div id=\"vehicle-detail\" hx-swap-oob=\"innerHTML\">\n      to update the detail panel out of band — one request, two updates. Using\n      innerHTML (not \"true\"/outerHTML) keeps the live #vehicle-detail node in the\n      DOM, swapping only its contents — so the aria-live region's identity is\n      preserved across updates.\n    - Relies only on theme tokens; no JS of its own.\n-->\n\n<fieldset data-slot=\"cascading-select\" id=\"vehicle\" class=\"grid gap-4\" aria-labelledby=\"vehicle-legend\">\n  <legend id=\"vehicle-legend\" class=\"mb-1 text-sm leading-none font-medium text-foreground\" data-slot=\"cascading-select-legend\">Vehicle</legend>\n\n  <!-- Parent -->\n  <div class=\"grid gap-2\">\n    <label for=\"vehicle-parent\" class=\"text-sm leading-none font-medium text-foreground select-none\">Make</label>\n    <span class=\"relative inline-flex w-full\">\n      <select id=\"vehicle-parent\" name=\"make\" data-slot=\"cascading-select-parent\"\n              hx-get=\"/models\" hx-target=\"#vehicle-child\" hx-include=\"[name='make']\"\n              aria-controls=\"vehicle-child vehicle-detail\"\n              class=\"peer flex h-9 w-full min-w-0 cursor-pointer appearance-none items-center rounded-md border border-input bg-background px-3 pr-8 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 [&.htmx-request]:opacity-70\">\n        <option value=\"audi\" selected>Audi</option>\n        <option value=\"toyota\">Toyota</option>\n        <option value=\"bmw\">BMW</option>\n      </select>\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n           class=\"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50\" aria-hidden=\"true\">\n        <polyline points=\"6 9 12 15 18 9\" />\n      </svg>\n    </span>\n  </div>\n\n  <!-- Child (htmx swaps fresh <option>s in here) -->\n  <div class=\"grid gap-2\">\n    <label for=\"vehicle-child\" class=\"text-sm leading-none font-medium text-foreground select-none\">Model</label>\n    <span class=\"relative inline-flex w-full\">\n      <select id=\"vehicle-child\" name=\"model\" data-slot=\"cascading-select-child\"\n              class=\"peer flex h-9 w-full min-w-0 cursor-pointer appearance-none items-center rounded-md border border-input bg-background px-3 pr-8 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 [&.htmx-request]:opacity-70\">\n        <option value=\"a4\" selected>A4</option>\n        <option value=\"q5\">Q5</option>\n        <option value=\"etron-gt\">e-tron GT</option>\n      </select>\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n           class=\"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50\" aria-hidden=\"true\">\n        <polyline points=\"6 9 12 15 18 9\" />\n      </svg>\n    </span>\n  </div>\n\n  <!-- Detail panel (updated out of band on each parent change) -->\n  <div id=\"vehicle-detail\" data-slot=\"cascading-select-detail\" aria-live=\"polite\" class=\"text-sm text-muted-foreground\">\n    Audi A4 — Sedan, $39,900\n  </div>\n</fieldset>\n\n<!--\n  The server returns, e.g.:\n\n    <option value=\"a4\" selected>A4</option>\n    <option value=\"q5\">Q5</option>\n    <option value=\"etron-gt\">e-tron GT</option>\n    <div id=\"vehicle-detail\" hx-swap-oob=\"innerHTML\" aria-live=\"polite\"\n         class=\"text-sm text-muted-foreground\">Audi A4 — Sedan, $39,900</div>\n-->\n"
    }
  ]
}
