{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "form-field",
  "type": "registry:ui",
  "title": "Form Field",
  "description": "Field-row wrapper composing label + control slot + description + error, auto-wiring aria-describedby and aria-invalid, with a native :user-invalid styling hook. Includes a fieldset/legend group variant. Ships in five flavours; zero JS.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/form-field.tsx",
      "type": "registry:ui",
      "target": "components/ui/form-field.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// Form Field — shadcn-htmx, htmx v4 + Tailwind v4.\n//\n// A field-row wrapper that composes a <label>, a single control slot, an\n// optional description, and an optional error message — auto-wiring the\n// label's `for`, the control's `aria-describedby` (description + error ids),\n// and `aria-invalid`. The visual error state is driven by the native\n// `:user-invalid` pseudo-class so the field only \"turns red\" AFTER the user\n// has interacted and a submit was attempted — no JS, no premature errors.\n//\n// Source of truth (shadcn anatomy — FormField / FormItem / FormLabel /\n// FormControl / FormDescription / FormMessage):\n//   repos/shadcn-ui/apps/v4/registry/new-york-v4/ui/form.tsx\n// Upstream couples those parts to react-hook-form via React context. We have\n// no client form runtime, so instead we lift the wiring to the server: one\n// component reads `id`/`invalid`/`description`/`error`, derives the ids, and\n// clones them onto the control child. Same semantic HTML, zero client state.\n//\n// Built on web platform primitives:\n//   - <fieldset>/<legend> for grouping multiple controls under one caption.\n//     repos/mdn/files/en-us/web/html/reference/elements/fieldset/index.md\n//     repos/mdn/files/en-us/web/html/reference/elements/legend/index.md\n//   - Constraint Validation + :user-invalid for \"show error only after the\n//     user tried\" styling. :invalid fires before interaction (confusing);\n//     :user-invalid fires only after a submit attempt + interaction.\n//     repos/mdn/files/en-us/web/css/reference/selectors/_colon_user-invalid/index.md\n//     repos/web.dev/src/site/content/en/learn/forms/validation/index.md (#javascript, :user-invalid aside)\n//   - aria-describedby to connect the control to its description + error.\n//     repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-describedby/index.md\n//     repos/web.dev/src/site/content/en/learn/forms/accessibility/index.md (\"Help users find the error message\")\n//\n// htmx: the field forwards hx-* untouched, so a server can swap the whole\n// field via hx-swap=\"outerHTML\" and flip aria-invalid + inject the error in\n// one shot. See repos/htmx/www/reference.md.\n\n// Root row. `grid gap-2` mirrors shadcn FormItem. The `[&:has(:user-invalid)]`\n// arbitrary selector lets the label adopt the destructive colour the moment\n// the platform marks any descendant control :user-invalid — pure CSS.\nconst fieldBase =\n  \"grid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive\"\n\nconst labelBase =\n  \"flex items-center gap-2 text-sm leading-none font-medium select-none \" +\n  // Author-driven error state (server sets data-invalid when it knows).\n  \"data-[invalid=true]:text-destructive \" +\n  // Dim when the control inside is disabled.\n  \"peer-disabled:cursor-not-allowed peer-disabled:opacity-50\"\n\nconst descriptionBase = \"text-sm text-muted-foreground\"\n\n// Error text. role=\"alert\" + aria-live=\"assertive\" so a swapped-in error is\n// announced; destructive colour pairs with the label turning red. Hidden when\n// empty so it doesn't leave a gap.\nconst errorBase = \"text-sm font-medium text-destructive\"\n\nconst legendBase = \"text-sm leading-none font-medium select-none\"\n\nexport function formFieldClasses(opts?: { class?: ClassValue }): string {\n  return cn(fieldBase, opts?.class)\n}\n\ntype FormFieldProps = {\n  // The single control to wire up (an <Input>, <Textarea>, <Select>, …).\n  // We clone it to inject id + aria-describedby + aria-invalid.\n  children?: Child\n  // Visible label text. Omit to render no label (e.g. control is self-labelled).\n  label?: Child\n  // The control's id. The label points at it via `for`, and the description /\n  // error ids are derived from it (`${id}-description`, `${id}-error`).\n  // Authored as the natural HTML attribute name `for`; `htmlFor` is accepted\n  // as an alias for ergonomics.\n  for?: string\n  htmlFor?: string\n  // Helper text under the label.\n  description?: Child\n  // Error message. When set (and `invalid` is not explicitly false) the field\n  // is marked aria-invalid and the message is announced.\n  error?: Child\n  // Force the invalid state. Defaults to `true` when `error` is provided.\n  invalid?: boolean\n  // Marks the label with a required indicator and is forwarded as data-required.\n  required?: boolean\n  class?: ClassValue\n  labelClass?: ClassValue\n  // htmx + data-* + aria-* ride along onto the root.\n  [key: string]: unknown\n}\n\nexport function FormField(props: FormFieldProps) {\n  const {\n    children,\n    label,\n    for: forProp,\n    htmlFor: htmlForProp,\n    description,\n    error,\n    invalid,\n    required,\n    class: className,\n    labelClass,\n    ...rest\n  } = props\n\n  // Accept the natural HTML attribute `for` (how the docs/routes author it),\n  // falling back to the `htmlFor` alias. Without this the id would leak onto\n  // the root div via {...rest} and the label/aria wiring would never derive.\n  const htmlFor = forProp ?? htmlForProp\n\n  const isInvalid = invalid ?? (error != null && error !== false)\n  const descriptionId = htmlFor && description != null ? `${htmlFor}-description` : undefined\n  const errorId = htmlFor && isInvalid && error != null ? `${htmlFor}-error` : undefined\n  // aria-describedby: description first, then error (announced after the name).\n  const describedby = [descriptionId, errorId].filter(Boolean).join(\" \") || undefined\n\n  // Clone the control child to inject the wiring. Mirrors the asChild pattern\n  // in registry/ui/button.tsx (hono/jsx cloneElement + isValidElement).\n  let control: Child = children\n  if (isValidElement(children)) {\n    const child = children as any\n    control = cloneElement(child, {\n      id: child?.props?.id ?? htmlFor,\n      \"aria-describedby\": cn(child?.props?.[\"aria-describedby\"], describedby) || undefined,\n      \"aria-invalid\": isInvalid ? \"true\" : child?.props?.[\"aria-invalid\"],\n      \"aria-required\": required ? \"true\" : child?.props?.[\"aria-required\"],\n    })\n  }\n\n  return (\n    <div\n      class={formFieldClasses({ class: className })}\n      data-slot=\"form-field\"\n      data-invalid={isInvalid ? \"true\" : undefined}\n      {...rest}\n    >\n      {label != null && (\n        <label\n          for={htmlFor}\n          class={cn(labelBase, labelClass)}\n          data-slot=\"form-field-label\"\n          data-invalid={isInvalid ? \"true\" : undefined}\n          data-required={required ? \"true\" : undefined}\n        >\n          {label}\n          {required && (\n            <span class=\"text-destructive\" aria-hidden=\"true\">\n              *\n            </span>\n          )}\n        </label>\n      )}\n      {control}\n      {description != null && (\n        <p id={descriptionId} class={descriptionBase} data-slot=\"form-field-description\">\n          {description}\n        </p>\n      )}\n      {isInvalid && error != null && (\n        <p\n          id={errorId}\n          role=\"alert\"\n          aria-live=\"assertive\"\n          class={errorBase}\n          data-slot=\"form-field-error\"\n        >\n          {error}\n        </p>\n      )}\n    </div>\n  )\n}\n\ntype FormFieldsetProps = {\n  children?: Child\n  // The <legend> caption for the group.\n  legend?: Child\n  description?: Child\n  error?: Child\n  invalid?: boolean\n  // Disables every control inside the group natively (fieldset[disabled]).\n  disabled?: boolean\n  required?: boolean\n  // id of the description/error so callers can point group controls at it.\n  id?: string\n  class?: ClassValue\n  legendClass?: ClassValue\n  [key: string]: unknown\n}\n\n// Fieldset variant — for grouping multiple controls (radios, checkboxes,\n// related inputs) under one caption. The <legend> names the group for AT, and\n// `disabled` on the <fieldset> disables every descendant control in one go.\n//   repos/mdn/files/en-us/web/html/reference/elements/fieldset/index.md\nexport function FormFieldset(props: FormFieldsetProps) {\n  const {\n    children,\n    legend,\n    description,\n    error,\n    invalid,\n    disabled,\n    required,\n    id,\n    class: className,\n    legendClass,\n    ...rest\n  } = props\n\n  const isInvalid = invalid ?? (error != null && error !== false)\n  const descriptionId = id && description != null ? `${id}-description` : undefined\n  const errorId = id && isInvalid && error != null ? `${id}-error` : undefined\n  const describedby = [descriptionId, errorId].filter(Boolean).join(\" \") || undefined\n\n  return (\n    <fieldset\n      class={cn(fieldBase, \"min-w-0 border-0 p-0\", className)}\n      data-slot=\"form-field\"\n      data-invalid={isInvalid ? \"true\" : undefined}\n      disabled={disabled}\n      aria-describedby={describedby}\n      aria-invalid={isInvalid ? \"true\" : undefined}\n      aria-required={required ? \"true\" : undefined}\n      {...rest}\n    >\n      {legend != null && (\n        <legend\n          class={cn(legendBase, \"float-none mb-1 data-[invalid=true]:text-destructive\", legendClass)}\n          data-slot=\"form-field-legend\"\n          data-invalid={isInvalid ? \"true\" : undefined}\n          data-required={required ? \"true\" : undefined}\n        >\n          {legend}\n          {required && (\n            <span class=\"text-destructive\" aria-hidden=\"true\">\n              {\" \"}\n              *\n            </span>\n          )}\n        </legend>\n      )}\n      {children}\n      {description != null && (\n        <p id={descriptionId} class={descriptionBase} data-slot=\"form-field-description\">\n          {description}\n        </p>\n      )}\n      {isInvalid && error != null && (\n        <p\n          id={errorId}\n          role=\"alert\"\n          aria-live=\"assertive\"\n          class={errorBase}\n          data-slot=\"form-field-error\"\n        >\n          {error}\n        </p>\n      )}\n    </fieldset>\n  )\n}\n"
    },
    {
      "path": "registry/jinja2/form-field.html",
      "type": "registry:file",
      "target": "templates/components/form-field.html",
      "content": "{# Form Field macros — shadcn-htmx, htmx v4 + Tailwind v4.\n   Mirrors registry/ui/form-field.tsx for Python/Flask/FastAPI/Django/Jinja2.\n\n   A field row composes a <label>, a control slot, an optional description and\n   an optional error. The label's `for`, the control's aria-describedby and\n   aria-invalid are wired from the id you pass. Error styling rides on the\n   native :user-invalid pseudo-class (no JS, no premature errors).\n\n   Usage (block form — you place the control yourself so its id matches):\n\n       {% from \"components/form-field.html\" import form_field, form_fieldset %}\n       {% from \"components/input.html\" import input %}\n\n       {% call form_field(for_=\"email\", label=\"Email\",\n                          description=\"We'll never share it.\",\n                          error=errors.email, required=true) %}\n         {{ input(id=\"email\", name=\"email\", type=\"email\",\n                  aria_describedby=\"email-description email-error\",\n                  aria_invalid=(errors.email is not none)) }}\n       {% endcall %}\n\n   Group form (multiple controls under one <legend>):\n\n       {% call form_fieldset(id=\"plan\", legend=\"Plan\", error=errors.plan) %}\n         …radios / checkboxes…\n       {% endcall %}\n\n   Built on <fieldset>/<legend>, Constraint Validation + :user-invalid, and\n   aria-describedby. See:\n     repos/mdn/files/en-us/web/html/reference/elements/fieldset/index.md\n     repos/mdn/files/en-us/web/css/reference/selectors/_colon_user-invalid/index.md\n     repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-describedby/index.md\n   htmx attrs pass through via **attrs (underscores become dashes). #}\n\n{% macro form_field(\n    for_=none,\n    label=none,\n    description=none,\n    error=none,\n    invalid=none,\n    required=false,\n    extra_class=\"\",\n    label_class=\"\",\n    **attrs\n) %}\n{%- set is_invalid = invalid if invalid is not none else (error is not none) -%}\n{%- set base -%}\ngrid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive\n{%- endset -%}\n<div class=\"{{ base }} {{ extra_class }}\"\n     data-slot=\"form-field\"\n     {%- if is_invalid %} data-invalid=\"true\"{% endif %}\n     {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}=\"{{ v }}\"{% endfor -%}\n>\n  {%- if label is not none %}\n  <label class=\"flex items-center gap-2 text-sm leading-none font-medium select-none data-[invalid=true]:text-destructive peer-disabled:cursor-not-allowed peer-disabled:opacity-50 {{ label_class }}\"\n         {%- if for_ %} for=\"{{ for_ }}\"{% endif %}\n         data-slot=\"form-field-label\"\n         {%- if is_invalid %} data-invalid=\"true\"{% endif %}\n         {%- if required %} data-required=\"true\"{% endif %}\n  >{{ label }}{% if required %}<span class=\"text-destructive\" aria-hidden=\"true\">*</span>{% endif %}</label>\n  {%- endif %}\n  {{ caller() }}\n  {%- if description is not none %}\n  <p {% if for_ %}id=\"{{ for_ }}-description\" {% endif %}class=\"text-sm text-muted-foreground\" data-slot=\"form-field-description\">{{ description }}</p>\n  {%- endif %}\n  {%- if is_invalid and error is not none %}\n  <p {% if for_ %}id=\"{{ for_ }}-error\" {% endif %}role=\"alert\" aria-live=\"assertive\" class=\"text-sm font-medium text-destructive\" data-slot=\"form-field-error\">{{ error }}</p>\n  {%- endif %}\n</div>\n{% endmacro %}\n\n{% macro form_fieldset(\n    id=none,\n    legend=none,\n    description=none,\n    error=none,\n    invalid=none,\n    disabled=false,\n    required=false,\n    extra_class=\"\",\n    legend_class=\"\",\n    **attrs\n) %}\n{%- set is_invalid = invalid if invalid is not none else (error is not none) -%}\n{%- set described = [] -%}\n{%- if id and description is not none %}{% set _ = described.append(id ~ \"-description\") %}{% endif -%}\n{%- if id and is_invalid and error is not none %}{% set _ = described.append(id ~ \"-error\") %}{% endif -%}\n{%- set base -%}\ngrid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive min-w-0 border-0 p-0\n{%- endset -%}\n<fieldset class=\"{{ base }} {{ extra_class }}\"\n          data-slot=\"form-field\"\n          {%- if is_invalid %} data-invalid=\"true\"{% endif %}\n          {%- if disabled %} disabled{% endif %}\n          {%- if described %} aria-describedby=\"{{ described|join(' ') }}\"{% endif %}\n          {%- if is_invalid %} aria-invalid=\"true\"{% endif %}\n          {%- if required %} aria-required=\"true\"{% endif %}\n          {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}=\"{{ v }}\"{% endfor -%}\n>\n  {%- if legend is not none %}\n  <legend class=\"text-sm leading-none font-medium select-none float-none mb-1 data-[invalid=true]:text-destructive {{ legend_class }}\"\n          data-slot=\"form-field-legend\"\n          {%- if is_invalid %} data-invalid=\"true\"{% endif %}\n          {%- if required %} data-required=\"true\"{% endif %}\n  >{{ legend }}{% if required %}<span class=\"text-destructive\" aria-hidden=\"true\"> *</span>{% endif %}</legend>\n  {%- endif %}\n  {{ caller() }}\n  {%- if description is not none %}\n  <p {% if id %}id=\"{{ id }}-description\" {% endif %}class=\"text-sm text-muted-foreground\" data-slot=\"form-field-description\">{{ description }}</p>\n  {%- endif %}\n  {%- if is_invalid and error is not none %}\n  <p {% if id %}id=\"{{ id }}-error\" {% endif %}role=\"alert\" aria-live=\"assertive\" class=\"text-sm font-medium text-destructive\" data-slot=\"form-field-error\">{{ error }}</p>\n  {%- endif %}\n</fieldset>\n{% endmacro %}\n"
    },
    {
      "path": "registry/go-templates/form-field.tmpl",
      "type": "registry:file",
      "target": "components/form-field.tmpl",
      "content": "{{/*\n  Form Field templates — shadcn-htmx, htmx v4 + Tailwind v4.\n  Mirrors registry/ui/form-field.tsx for Go projects using html/template.\n\n  A field row composes a <label>, a control slot, an optional description, and\n  an optional error. The label's `for`, and the control's aria-describedby /\n  aria-invalid, are wired from the id you pass. Error styling rides on the\n  native :user-invalid pseudo-class. Because html/template has no caller-block\n  syntax, hand the control HTML in via .Control (template.HTML).\n\n  Usage in your handler:\n\n      type FormFieldArgs struct {\n          For         string        // id of the control inside .Control\n          Label       string\n          Description string\n          Error       string        // non-empty => invalid\n          Invalid     bool          // force invalid even without a message\n          Required    bool\n          Control     template.HTML // the <input>/<select>/… markup\n      }\n\n      tpl.ExecuteTemplate(w, \"form-field\", FormFieldArgs{\n          For: \"email\", Label: \"Email\",\n          Description: \"We'll never share it.\",\n          Error: errs[\"email\"],\n          Control: template.HTML(`<input id=\"email\" name=\"email\" type=\"email\"\n              aria-describedby=\"email-description email-error\" …>`),\n      })\n\n  Built on <fieldset>/<legend>, Constraint Validation + :user-invalid, and\n  aria-describedby. See:\n    repos/mdn/files/en-us/web/html/reference/elements/fieldset/index.md\n    repos/mdn/files/en-us/web/css/reference/selectors/_colon_user-invalid/index.md\n    repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-describedby/index.md\n*/}}\n\n{{define \"form-field\"}}\n{{- $invalid := or .Invalid (ne (or .Error \"\") \"\") -}}\n{{- $base := \"grid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive\" -}}\n<div class=\"{{$base}}\" data-slot=\"form-field\"{{if $invalid}} data-invalid=\"true\"{{end}}>\n  {{- if .Label}}\n  <label class=\"flex items-center gap-2 text-sm leading-none font-medium select-none data-[invalid=true]:text-destructive peer-disabled:cursor-not-allowed peer-disabled:opacity-50\"\n         {{- if .For}} for=\"{{.For}}\"{{end}} data-slot=\"form-field-label\"{{if $invalid}} data-invalid=\"true\"{{end}}{{if .Required}} data-required=\"true\"{{end}}>{{.Label}}{{if .Required}}<span class=\"text-destructive\" aria-hidden=\"true\">*</span>{{end}}</label>\n  {{- end}}\n  {{- if .Control}}{{htmlSafe .Control}}{{end}}\n  {{- if .Description}}\n  <p {{if .For}}id=\"{{.For}}-description\" {{end}}class=\"text-sm text-muted-foreground\" data-slot=\"form-field-description\">{{.Description}}</p>\n  {{- end}}\n  {{- if and $invalid (ne (or .Error \"\") \"\")}}\n  <p {{if .For}}id=\"{{.For}}-error\" {{end}}role=\"alert\" aria-live=\"assertive\" class=\"text-sm font-medium text-destructive\" data-slot=\"form-field-error\">{{.Error}}</p>\n  {{- end}}\n</div>\n{{end}}\n\n{{/*\n  Group variant — multiple controls under one <legend>. `Disabled` disables\n  every descendant control natively. Point group controls at\n  \"<ID>-description <ID>-error\" via their own aria-describedby.\n\n      tpl.ExecuteTemplate(w, \"form-fieldset\", FormFieldsetArgs{\n          ID: \"plan\", Legend: \"Plan\", Error: errs[\"plan\"],\n          Controls: template.HTML(`…radios…`),\n      })\n*/}}\n{{define \"form-fieldset\"}}\n{{- $invalid := or .Invalid (ne (or .Error \"\") \"\") -}}\n{{- $base := \"grid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive min-w-0 border-0 p-0\" -}}\n{{- $hasDesc := ne (or .Description \"\") \"\" -}}\n{{- $hasErr := and $invalid (ne (or .Error \"\") \"\") -}}\n<fieldset class=\"{{$base}}\" data-slot=\"form-field\"{{if $invalid}} data-invalid=\"true\"{{end}}{{if .Disabled}} disabled{{end}}\n  {{- if .ID}}{{if $hasDesc}} aria-describedby=\"{{.ID}}-description{{if $hasErr}} {{.ID}}-error{{end}}\"{{else if $hasErr}} aria-describedby=\"{{.ID}}-error\"{{end}}{{end}}\n  {{- if $invalid}} aria-invalid=\"true\"{{end}}{{if .Required}} aria-required=\"true\"{{end}}>\n  {{- if .Legend}}\n  <legend class=\"text-sm leading-none font-medium select-none float-none mb-1 data-[invalid=true]:text-destructive\" data-slot=\"form-field-legend\"{{if $invalid}} data-invalid=\"true\"{{end}}{{if .Required}} data-required=\"true\"{{end}}>{{.Legend}}{{if .Required}}<span class=\"text-destructive\" aria-hidden=\"true\"> *</span>{{end}}</legend>\n  {{- end}}\n  {{- if .Controls}}{{htmlSafe .Controls}}{{end}}\n  {{- if .Description}}\n  <p {{if .ID}}id=\"{{.ID}}-description\" {{end}}class=\"text-sm text-muted-foreground\" data-slot=\"form-field-description\">{{.Description}}</p>\n  {{- end}}\n  {{- if $hasErr}}\n  <p {{if .ID}}id=\"{{.ID}}-error\" {{end}}role=\"alert\" aria-live=\"assertive\" class=\"text-sm font-medium text-destructive\" data-slot=\"form-field-error\">{{.Error}}</p>\n  {{- end}}\n</fieldset>\n{{end}}\n"
    },
    {
      "path": "registry/phoenix/form_field.ex",
      "type": "registry:file",
      "target": "lib/my_app_web/components/form_field.ex",
      "content": "defmodule ShadcnHtmx.Components.FormField do\n  @moduledoc \"\"\"\n  Form Field — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.\n\n  Mirrors registry/ui/form-field.tsx. A field row that composes a `<label>`,\n  a control slot, an optional description, and an optional error. The label's\n  `for`, plus the control's `aria-describedby` / `aria-invalid`, are wired from\n  the `for`/`id` you pass. Error styling rides on the native `:user-invalid`\n  pseudo-class — the field only turns red after the user interacts and a submit\n  is attempted, so no premature errors and no JS.\n\n  ## Examples\n\n      <.form_field for=\"email\" label=\"Email\"\n                   description=\"We'll never share it.\"\n                   error={@errors[:email]} required>\n        <.input id=\"email\" name=\"email\" type=\"email\"\n                aria-describedby=\"email-description email-error\"\n                aria-invalid={@errors[:email] && \"true\"} />\n      </.form_field>\n\n      <.form_fieldset id=\"plan\" legend=\"Plan\" error={@errors[:plan]}>\n        <%!-- radios / checkboxes --%>\n      </.form_fieldset>\n\n  Built on `<fieldset>`/`<legend>`, Constraint Validation + `:user-invalid`,\n  and `aria-describedby`. See:\n    repos/mdn/files/en-us/web/html/reference/elements/fieldset/index.md\n    repos/mdn/files/en-us/web/css/reference/selectors/_colon_user-invalid/index.md\n    repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-describedby/index.md\n  \"\"\"\n\n  use Phoenix.Component\n\n  @field_base \"grid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive\"\n  @label_base \"flex items-center gap-2 text-sm leading-none font-medium select-none \" <>\n                \"data-[invalid=true]:text-destructive \" <>\n                \"peer-disabled:cursor-not-allowed peer-disabled:opacity-50\"\n  @legend_base \"text-sm leading-none font-medium select-none float-none mb-1 data-[invalid=true]:text-destructive\"\n\n  attr :for, :string, default: nil, doc: \"id of the control; the label points at it\"\n  attr :label, :string, default: nil\n  attr :description, :string, default: nil\n  attr :error, :string, default: nil, doc: \"non-nil => invalid\"\n  attr :invalid, :boolean, default: nil, doc: \"force invalid without a message\"\n  attr :required, :boolean, default: false\n  attr :class, :string, default: nil\n  attr :label_class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block, required: true\n\n  def form_field(assigns) do\n    assigns =\n      assigns\n      |> assign(:is_invalid, if(assigns.invalid != nil, do: assigns.invalid, else: assigns.error != nil))\n      |> assign(:field_base, @field_base)\n      |> assign(:label_base, @label_base)\n\n    ~H\"\"\"\n    <div\n      class={[@field_base, @class]}\n      data-slot=\"form-field\"\n      data-invalid={@is_invalid && \"true\"}\n      {@rest}\n    >\n      <label\n        :if={@label}\n        for={@for}\n        class={[@label_base, @label_class]}\n        data-slot=\"form-field-label\"\n        data-invalid={@is_invalid && \"true\"}\n        data-required={@required && \"true\"}\n      >\n        {@label}<span :if={@required} class=\"text-destructive\" aria-hidden=\"true\">*</span>\n      </label>\n      {render_slot(@inner_block)}\n      <p\n        :if={@description}\n        id={@for && \"#{@for}-description\"}\n        class=\"text-sm text-muted-foreground\"\n        data-slot=\"form-field-description\"\n      >\n        {@description}\n      </p>\n      <p\n        :if={@is_invalid && @error}\n        id={@for && \"#{@for}-error\"}\n        role=\"alert\"\n        aria-live=\"assertive\"\n        class=\"text-sm font-medium text-destructive\"\n        data-slot=\"form-field-error\"\n      >\n        {@error}\n      </p>\n    </div>\n    \"\"\"\n  end\n\n  attr :id, :string, default: nil\n  attr :legend, :string, default: nil\n  attr :description, :string, default: nil\n  attr :error, :string, default: nil\n  attr :invalid, :boolean, default: nil\n  attr :disabled, :boolean, default: false\n  attr :required, :boolean, default: false\n  attr :class, :string, default: nil\n  attr :legend_class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block, required: true\n\n  def form_fieldset(assigns) do\n    is_invalid = if assigns.invalid != nil, do: assigns.invalid, else: assigns.error != nil\n\n    described =\n      [\n        assigns.id && assigns.description && \"#{assigns.id}-description\",\n        assigns.id && is_invalid && assigns.error && \"#{assigns.id}-error\"\n      ]\n      |> Enum.filter(& &1)\n\n    assigns =\n      assigns\n      |> assign(:is_invalid, is_invalid)\n      |> assign(:describedby, if(described == [], do: nil, else: Enum.join(described, \" \")))\n      |> assign(:field_base, @field_base)\n      |> assign(:legend_base, @legend_base)\n\n    ~H\"\"\"\n    <fieldset\n      class={[@field_base, \"min-w-0 border-0 p-0\", @class]}\n      data-slot=\"form-field\"\n      data-invalid={@is_invalid && \"true\"}\n      disabled={@disabled}\n      aria-describedby={@describedby}\n      aria-invalid={@is_invalid && \"true\"}\n      aria-required={@required && \"true\"}\n      {@rest}\n    >\n      <legend\n        :if={@legend}\n        class={[@legend_base, @legend_class]}\n        data-slot=\"form-field-legend\"\n        data-invalid={@is_invalid && \"true\"}\n        data-required={@required && \"true\"}\n      >\n        {@legend}<span :if={@required} class=\"text-destructive\" aria-hidden=\"true\"> *</span>\n      </legend>\n      {render_slot(@inner_block)}\n      <p\n        :if={@description}\n        id={@id && \"#{@id}-description\"}\n        class=\"text-sm text-muted-foreground\"\n        data-slot=\"form-field-description\"\n      >\n        {@description}\n      </p>\n      <p\n        :if={@is_invalid && @error}\n        id={@id && \"#{@id}-error\"}\n        role=\"alert\"\n        aria-live=\"assertive\"\n        class=\"text-sm font-medium text-destructive\"\n        data-slot=\"form-field-error\"\n      >\n        {@error}\n      </p>\n    </fieldset>\n    \"\"\"\n  end\nend\n"
    },
    {
      "path": "registry/html/form-field.html",
      "type": "registry:file",
      "target": "snippets/form-field.html",
      "content": "<!--\n  shadcn-htmx — raw Form Field snippets.\n\n  Mirrors registry/ui/form-field.tsx. Drop these onto any page that loads\n  Tailwind CSS v4 and the shadcn theme variables (foreground, muted-foreground,\n  input, ring, destructive). The label's `for`, the control's\n  aria-describedby, and aria-invalid are wired by hand here — keep the ids in\n  sync. Error styling rides on the native :user-invalid pseudo so the field\n  only turns red AFTER the user interacts + attempts submit. No JS required.\n\n  Built on <fieldset>/<legend>, Constraint Validation + :user-invalid, and\n  aria-describedby. See:\n    repos/mdn/files/en-us/web/html/reference/elements/fieldset/index.md\n    repos/mdn/files/en-us/web/css/reference/selectors/_colon_user-invalid/index.md\n    repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-describedby/index.md\n\n  The :user-invalid hook (drives the label red, pure CSS):\n    [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive\n-->\n\n<!-- ─── Single field — label + input + description ─────────────────────── -->\n<div data-slot=\"form-field\"\n     class=\"grid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive\">\n  <label for=\"ff-email\" data-slot=\"form-field-label\"\n         class=\"flex items-center gap-2 text-sm leading-none font-medium select-none data-[invalid=true]:text-destructive peer-disabled:cursor-not-allowed peer-disabled:opacity-50\">\n    Email\n  </label>\n  <input id=\"ff-email\" name=\"email\" type=\"email\" required autocomplete=\"email\"\n         placeholder=\"you@example.com\" data-slot=\"input\"\n         aria-describedby=\"ff-email-description\"\n         class=\"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 user-invalid:border-destructive\">\n  <p id=\"ff-email-description\" class=\"text-sm text-muted-foreground\" data-slot=\"form-field-description\">\n    We'll never share it.\n  </p>\n</div>\n\n<!-- ─── Invalid field — server-known error (data-invalid + role=alert) ──── -->\n<div data-slot=\"form-field\" data-invalid=\"true\"\n     class=\"grid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive\">\n  <label for=\"ff-name\" data-slot=\"form-field-label\" data-invalid=\"true\" data-required=\"true\"\n         class=\"flex items-center gap-2 text-sm leading-none font-medium select-none data-[invalid=true]:text-destructive peer-disabled:cursor-not-allowed peer-disabled:opacity-50\">\n    Full name<span class=\"text-destructive\" aria-hidden=\"true\">*</span>\n  </label>\n  <input id=\"ff-name\" name=\"name\" type=\"text\" value=\"\" aria-invalid=\"true\" aria-required=\"true\"\n         aria-describedby=\"ff-name-error\" data-slot=\"input\"\n         class=\"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\">\n  <p id=\"ff-name-error\" role=\"alert\" aria-live=\"assertive\" class=\"text-sm font-medium text-destructive\" data-slot=\"form-field-error\">\n    Name is required.\n  </p>\n</div>\n\n<!-- ─── Fieldset group — multiple controls under one legend ────────────── -->\n<fieldset data-slot=\"form-field\"\n          class=\"grid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive min-w-0 border-0 p-0\">\n  <legend data-slot=\"form-field-legend\"\n          class=\"text-sm leading-none font-medium select-none float-none mb-1 data-[invalid=true]:text-destructive\">\n    Notification method\n  </legend>\n  <label class=\"flex items-center gap-2 text-sm font-medium\">\n    <input type=\"radio\" name=\"notify\" value=\"email\" class=\"size-4\" required> Email\n  </label>\n  <label class=\"flex items-center gap-2 text-sm font-medium\">\n    <input type=\"radio\" name=\"notify\" value=\"sms\" class=\"size-4\"> SMS\n  </label>\n  <p class=\"text-sm text-muted-foreground\" data-slot=\"form-field-description\">\n    Choose how we reach you.\n  </p>\n</fieldset>\n\n<!-- ─── htmx — validate on blur, swap the whole field ──────────────────── -->\n<!-- The server returns the same field markup, flipping aria-invalid + the\n     error <p> in one outerHTML swap. -->\n<div id=\"ff-htmx\" data-slot=\"form-field\"\n     class=\"grid gap-2 [&:has(:user-invalid)_[data-slot=form-field-label]]:text-destructive\">\n  <label for=\"ff-htmx-email\" data-slot=\"form-field-label\"\n         class=\"flex items-center gap-2 text-sm leading-none font-medium select-none data-[invalid=true]:text-destructive\">\n    Email (checked on blur)\n  </label>\n  <input id=\"ff-htmx-email\" name=\"email\" type=\"email\" placeholder=\"you@example.com\" data-slot=\"input\"\n         hx-post=\"/api/validate-email\" hx-trigger=\"blur\" hx-target=\"#ff-htmx\" hx-swap=\"outerHTML\"\n         class=\"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&.htmx-request]:opacity-70\">\n</div>\n"
    }
  ]
}
