{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "field",
  "title": "Field",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Form field layout with label, description and error that wire their ids to the control automatically, plus fieldset, legend, orientation and invalid/disabled state.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/field.tsx",
      "content": "// Ballmac UI: Field. https://ui.ballmac.com/components/field\n// Based on shadcn/ui Field (MIT, Copyright (c) 2023 shadcn), adding automatic id wiring (label, description, error) through useFieldControl and an invalid/disabled state that flows to the control.\n\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\ntype FieldContextValue = {\n  id: string;\n  invalid: boolean;\n  disabled: boolean;\n};\nconst FieldContext = React.createContext<FieldContextValue | null>(null);\n\ntype FieldControlProps = {\n  id?: string;\n  \"aria-invalid\"?: true;\n  \"aria-describedby\"?: string;\n  disabled?: true;\n};\n\n/**\n * Props to spread on the control inside a `Field` (Input, Textarea, Select trigger, Combobox…).\n * It links the control to the label, description and error, and passes the invalid and disabled state.\n * Returns an empty object outside a `Field`.\n */\nfunction useFieldControl(): FieldControlProps {\n  const field = React.useContext(FieldContext);\n  if (!field) return {};\n  return {\n    id: field.id,\n    \"aria-invalid\": field.invalid || undefined,\n    \"aria-describedby\":\n      [`${field.id}-description`, field.invalid ? `${field.id}-error` : null]\n        .filter(Boolean)\n        .join(\" \") || undefined,\n    disabled: field.disabled || undefined,\n  };\n}\n\ntype FieldSetProps = React.ComponentProps<\"fieldset\">;\nfunction FieldSet({ className, ...props }: FieldSetProps) {\n  return (\n    <fieldset\n      data-slot=\"field-set\"\n      className={cn(\"flex flex-col gap-5\", className)}\n      {...props}\n    />\n  );\n}\n\ntype FieldLegendProps = React.ComponentProps<\"legend\"> & {\n  /** `legend` for a section title, `label` for a smaller heading. */\n  variant?: \"legend\" | \"label\";\n};\nfunction FieldLegend({ className, variant = \"legend\", ...props }: FieldLegendProps) {\n  return (\n    <legend\n      data-slot=\"field-legend\"\n      data-variant={variant}\n      className={cn(\n        \"mb-3 font-semibold data-[variant=label]:text-sm data-[variant=legend]:text-base\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ntype FieldGroupProps = React.ComponentProps<\"div\">;\nfunction FieldGroup({ className, ...props }: FieldGroupProps) {\n  return (\n    <div\n      data-slot=\"field-group\"\n      className={cn(\"group/field-group flex w-full flex-col gap-5\", className)}\n      {...props}\n    />\n  );\n}\n\nconst fieldVariants = cva(\"group/field flex w-full gap-2 data-[invalid=true]:text-destructive\", {\n  variants: {\n    orientation: {\n      vertical: \"flex-col\",\n      horizontal: \"flex-row items-center [&>[data-slot=field-label]]:flex-auto\",\n      responsive:\n        \"flex-col @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>[data-slot=field-label]]:flex-auto\",\n    },\n  },\n  defaultVariants: { orientation: \"vertical\" },\n});\n\ntype FieldProps = React.ComponentProps<\"div\"> &\n  VariantProps<typeof fieldVariants> & {\n    /** Layout of label and control. `horizontal` suits checkboxes and switches. */\n    orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n    /** Marks the control invalid: the label turns destructive, the control gets `aria-invalid` and the error is announced. */\n    invalid?: boolean;\n    /** Disables the control and dims the label. */\n    disabled?: boolean;\n    /** Override the generated id shared by the label and control. */\n    controlId?: string;\n  };\nfunction Field({\n  className,\n  orientation = \"vertical\",\n  invalid = false,\n  disabled = false,\n  controlId,\n  ...props\n}: FieldProps) {\n  const generated = React.useId();\n  const id = controlId ?? generated;\n  const value = React.useMemo(() => ({ id, invalid, disabled }), [id, invalid, disabled]);\n  return (\n    <FieldContext.Provider value={value}>\n      <div\n        role=\"group\"\n        data-slot=\"field\"\n        data-orientation={orientation}\n        data-invalid={invalid}\n        data-disabled={disabled}\n        className={cn(\n          fieldVariants({ orientation }),\n          \"data-[disabled=true]:opacity-60\",\n          className,\n        )}\n        {...props}\n      />\n    </FieldContext.Provider>\n  );\n}\n\ntype FieldContentProps = React.ComponentProps<\"div\">;\n/** Groups the label and description beside a horizontal control. */\nfunction FieldContent({ className, ...props }: FieldContentProps) {\n  return (\n    <div\n      data-slot=\"field-content\"\n      className={cn(\"flex flex-1 flex-col gap-1 leading-snug\", className)}\n      {...props}\n    />\n  );\n}\n\ntype FieldLabelProps = React.ComponentProps<\"label\"> & {\n  /** Show a required asterisk after the text. Add `required` to the control too. */\n  required?: boolean;\n};\nfunction FieldLabel({ className, required, children, htmlFor, ...props }: FieldLabelProps) {\n  const field = React.useContext(FieldContext);\n  return (\n    <label\n      data-slot=\"field-label\"\n      htmlFor={htmlFor ?? field?.id}\n      className={cn(\n        \"flex w-fit select-none items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:cursor-not-allowed\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {required && (\n        <span aria-hidden=\"true\" className=\"text-destructive\">\n          *\n        </span>\n      )}\n    </label>\n  );\n}\n\ntype FieldTitleProps = React.ComponentProps<\"div\">;\n/** A label-styled title for cards that hold a control (radio cards, switch rows). */\nfunction FieldTitle({ className, ...props }: FieldTitleProps) {\n  return (\n    <div\n      data-slot=\"field-title\"\n      className={cn(\"flex w-fit items-center gap-2 text-sm leading-snug font-medium\", className)}\n      {...props}\n    />\n  );\n}\n\ntype FieldDescriptionProps = React.ComponentProps<\"p\">;\nfunction FieldDescription({ className, id, ...props }: FieldDescriptionProps) {\n  const field = React.useContext(FieldContext);\n  return (\n    <p\n      data-slot=\"field-description\"\n      id={id ?? (field ? `${field.id}-description` : undefined)}\n      className={cn(\"text-sm leading-normal font-normal text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\ntype FieldErrorProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Messages to show. Duplicates are removed; one message renders as text, several as a list. */\n  errors?: Array<{ message?: string } | string | undefined>;\n  /** Custom content instead of `errors`. */\n  children?: React.ReactNode;\n};\n/** Announces validation messages (`role=\"alert\"`). Renders nothing when there are none. */\nfunction FieldError({ className, errors, children, id, ...props }: FieldErrorProps) {\n  const field = React.useContext(FieldContext);\n  const messages = React.useMemo(() => {\n    const list = (errors ?? [])\n      .map((e) => (typeof e === \"string\" ? e : e?.message))\n      .filter((m): m is string => Boolean(m));\n    return [...new Set(list)];\n  }, [errors]);\n  const content =\n    children ??\n    (messages.length === 0 ? null : messages.length === 1 ? (\n      messages[0]\n    ) : (\n      <ul className=\"ms-4 flex list-disc flex-col gap-1\">\n        {messages.map((m) => (\n          <li key={m}>{m}</li>\n        ))}\n      </ul>\n    ));\n  if (!content) return null;\n  return (\n    <div\n      role=\"alert\"\n      data-slot=\"field-error\"\n      id={id ?? (field ? `${field.id}-error` : undefined)}\n      className={cn(\"text-sm font-normal text-destructive\", className)}\n      {...props}\n    >\n      {content}\n    </div>\n  );\n}\n\ntype FieldSeparatorProps = React.ComponentProps<\"div\"> & {\n  /** Text in the middle of the rule, for example \"or\". */\n  children?: React.ReactNode;\n};\nfunction FieldSeparator({ children, className, ...props }: FieldSeparatorProps) {\n  return (\n    <div\n      data-slot=\"field-separator\"\n      aria-hidden=\"true\"\n      className={cn(\"relative -my-1 flex h-5 items-center text-sm\", className)}\n      {...props}\n    >\n      <span className=\"absolute inset-x-0 h-px bg-border\" />\n      {children && (\n        <span className=\"relative mx-auto bg-background px-2 text-muted-foreground\">\n          {children}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport {\n  Field,\n  FieldLabel,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLegend,\n  FieldSeparator,\n  FieldSet,\n  FieldContent,\n  FieldTitle,\n  useFieldControl,\n  fieldVariants,\n  type FieldControlProps,\n  type FieldProps,\n  type FieldLabelProps,\n  type FieldDescriptionProps,\n  type FieldErrorProps,\n  type FieldGroupProps,\n  type FieldLegendProps,\n  type FieldSeparatorProps,\n  type FieldSetProps,\n  type FieldContentProps,\n  type FieldTitleProps,\n};\n",
      "type": "registry:ui",
      "target": "@components/ballmac/field.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "form",
      "label",
      "validation",
      "layout"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "Wrap a control in Field, spread useFieldControl() on it, and label, description, error, aria-invalid and disabled are connected for you.",
      "whenToUse": [
        "Any labelled form control",
        "Forms with validation messages",
        "Settings rows with a switch or checkbox"
      ],
      "whenNotToUse": [
        "A single unlabeled search box; use search-field",
        "Complete form state management; combine with your form library"
      ],
      "composesWith": [
        "input",
        "textarea",
        "select",
        "checkbox",
        "switch",
        "combobox"
      ],
      "a11y": [
        {
          "keys": "Click label",
          "action": "Focuses the control"
        },
        {
          "keys": "Screen readers",
          "action": "Read the description, and the error when invalid; the error is announced with role alert"
        }
      ],
      "customization": [
        "orientation: vertical | horizontal | responsive",
        "invalid and disabled",
        "FieldError errors array (deduplicated)",
        "required marker on FieldLabel"
      ]
    },
    "source": {
      "name": "shadcn/ui Field",
      "url": "https://github.com/shadcn-ui/ui",
      "license": "MIT",
      "copyright": "Copyright (c) 2023 shadcn",
      "modified": true
    },
    "examples": [
      "field-demo",
      "field-states"
    ],
    "url": "https://ui.ballmac.com/components/field"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}