{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"field","type":"registry:ui","title":"Field","description":"Wraps a control with its label, hint, error and optional action, wired for assistive tech.","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/field.tsx","type":"registry:ui","target":"components/ui/field.tsx","content":"\"use client\";\nimport { CircleAlert, CircleCheck, LoaderCircle } from \"lucide-react\";\nimport { useId, type ReactNode } from \"react\";\nimport { Label } from \"./label\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/field-message.css\";\nimport \"./shared/loading-indicator.css\";\nimport \"./field.css\";\n\nexport type FieldControlProps = {\n  id: string;\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: true;\n  \"aria-required\"?: true;\n};\n\nexport const isEmail = (value: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim());\n\n/**\n * A label, a control and one message line wired together for assistive\n * technology. Hint, async status and error share the same reserved line and\n * rewrite it in place, so validation never pushes the next field down.\n */\nexport function Field({\n  label,\n  description,\n  error,\n  status,\n  statusTone = \"pending\",\n  count,\n  required,\n  optional,\n  action,\n  className,\n  children,\n}: {\n  label: string;\n  /** Hint shown on the message line until a status or error replaces it. */\n  description?: ReactNode;\n  /** Replaces the hint and is announced when it appears. */\n  error?: string;\n  /** An async result such as “Checking availability…” or “Available”. */\n  status?: string;\n  statusTone?: \"pending\" | \"success\";\n  /** Right-aligned counter on the message line, e.g. “6 / 40”. */\n  count?: string;\n  required?: boolean;\n  /** Adds \"(optional)\" after the label. */\n  optional?: boolean;\n  /** Something aligned with the label, such as a \"Forgot password?\" link. */\n  action?: ReactNode;\n  className?: string;\n  children: (props: FieldControlProps) => ReactNode;\n}) {\n  const id = useId();\n  const message = `${id}-message`;\n  const hasMessage = Boolean(error || status || description || count);\n  const tone = error ? \"error\" : status ? statusTone : \"hint\";\n  return (\n    <div data-slot=\"field\" className={cn(\"grid gap-1.5\", className)}>\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <Label htmlFor={id}>\n          {label}\n          {optional && <span className=\"ml-1 font-normal text-muted-foreground\">(optional)</span>}\n        </Label>\n        {action}\n      </div>\n      {children({\n        id,\n        \"aria-describedby\": hasMessage ? message : undefined,\n        \"aria-invalid\": error ? true : undefined,\n        \"aria-required\": required ? true : undefined,\n      })}\n      <p id={message} data-slot=\"field-message\" data-tone={tone} aria-live={error ? \"assertive\" : \"polite\"} role={error ? \"alert\" : undefined}>\n        {tone === \"error\" && <CircleAlert aria-hidden=\"true\" />}\n        {tone === \"success\" && <CircleCheck aria-hidden=\"true\" />}\n        {tone === \"pending\" && <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />}\n        <span data-slot=\"field-error\" className=\"min-w-0 flex-1\">{error ?? status ?? description}</span>\n        {count && <span data-slot=\"field-count\">{count}</span>}\n      </p>\n    </div>\n  );\n}\n"},{"path":"ui/label.tsx","type":"registry:ui","target":"components/ui/label.tsx","content":"import type { ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\n\nexport function Label({ className, ...props }: ComponentProps<\"label\">) {\n  return (\n    <label\n      data-slot=\"label\"\n      className={cn(\n        \"inline-block max-w-full text-sm leading-5 font-medium [overflow-wrap:anywhere]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"},{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/shared/field-message.css","type":"registry:file","target":"components/ui/shared/field-message.css","content":"/* Shared by date-input.tsx, field.tsx, scrub-field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"field-message\"] {\n    display: flex;\n    align-items: flex-start;\n    gap: 6px;\n    min-height: 18px;\n    margin: 0;\n    font-size: 12px;\n    line-height: 18px;\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"field-message\"] svg {\n    flex: none;\n    width: 14px;\n    height: 14px;\n    margin-top: 2px;\n  }\n  [data-slot=\"field-message\"][data-tone=\"error\"] {\n    color: var(--danger);\n  }\n  [data-slot=\"field-message\"][data-tone=\"success\"] {\n    color: var(--success);\n  }\n}\n"},{"path":"ui/shared/loading-indicator.css","type":"registry:file","target":"components/ui/shared/loading-indicator.css","content":"/* Shared by button.tsx, confirm-morph.tsx, expanding-button-group.tsx, field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"loading-indicator\"] {\n    animation: caliper-spin var(--duration-loading) var(--ease-linear) infinite;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"loading-indicator\"] {\n      animation: none;\n    }\n  }\n}\n"},{"path":"ui/field.css","type":"registry:file","target":"components/ui/field.css","content":"/* field.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"field-count\"] {\n    margin-left: auto;\n    color: var(--muted-foreground);\n    font-variant-numeric: tabular-nums;\n    font-size: 12px;\n    font-variant-numeric: tabular-nums;\n  }\n}\n"}],"categories":["inputs"],"meta":{"tier":"free","license":"MIT","family":"inputs","docs":"/docs/components/field"}}