# Form

> Form fields wired to react-hook-form: each field gets a label, a control, an optional hint and an error message, with ids and ARIA linked for you.

## Facts

- **Product**: Crew OS
- **Family**: Forms
- **Global**: `window.DigitalCrew.Form`
- **Source**: `digitalcrew-orchestrator: components/ui/form.tsx`
- **Import in the app**: `import { Form } from "@/components/ui/form";`
- **Live preview**: /design/components/form/preview.html
- **Page**: /design/components/form

## Guidelines

**Use it for** any settings or creation form in Crew OS (campaign, mailbox, agent settings), with validation from a zod resolver or `setError` from the server.

**What you provide**: `const form = useForm({ defaultValues })` (`DC.useForm` in this bundle), then `<Form {...form}>` (it is `FormProvider`) around a `<form onSubmit={form.handleSubmit(...)}>`. Per field: `FormField` with `control`, `name` and `render={({ field }) => …}` returning `FormItem` › `FormLabel`, `FormControl` wrapping exactly one input (spread `field` into it), `FormDescription`, `FormMessage`.

**Anatomy**: `FormItem` is a grid with 8px gaps. `FormLabel` turns `text-destructive` when the field has an error (`data-error`). `FormControl` is a Slot: it sets the input's `id`, `aria-invalid` and `aria-describedby` (the description, plus the message when invalid), so inputs draw their own invalid ring. `FormDescription` is `text-sm text-muted-foreground`; `FormMessage` is `text-sm text-destructive`, shows the error's `message` (or its own children) and renders nothing when empty.

**Don't** put two controls in one `FormControl`, repeat the error in a toast, or hide the description when an error shows (both are announced).

## API

```ts
declare const Form: <TFieldValues extends FieldValues, TContext = any, TTransformedValues = TFieldValues>(props: FormProviderProps<TFieldValues, TContext, TTransformedValues>) => React.JSX.Element;
declare const FormField: <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>({ ...props }: ControllerProps<TFieldValues, TName>) => any;
declare const useFormField: () => {
    invalid: boolean;
    isDirty: boolean;
    isTouched: boolean;
    isValidating: boolean;
    error?: FieldError;
    id: any;
    name: any;
    formItemId: string;
    formDescriptionId: string;
    formMessageId: string;
};
declare function FormItem({ className, ...props }: React.ComponentProps<"div">): any;
declare function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>): any;
declare function FormControl({ ...props }: React.ComponentProps<typeof Slot>): any;
declare function FormDescription({ className, ...props }: React.ComponentProps<"p">): any;
declare function FormMessage({ className, ...props }: React.ComponentProps<"p">): any;
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField, };
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  function App() {
    var form = DC.useForm({ defaultValues: { name: "Q3 CFO outreach", sender: "claire@northwind-mail.io" } });
    React.useEffect(function () {
      form.setError("sender", { type: "manual", message: "Use a mailbox on a verified sending domain." });
    }, []);
    function field(name, label, hint, type) {
      return h(DC.FormField, { control: form.control, name: name, render: function (p) {
        return h(DC.FormItem, null,
          h(DC.FormLabel, null, label),
          h(DC.FormControl, null, h(DC.Input, Object.assign({ type: type || "text" }, p.field))),
          h(DC.FormDescription, null, hint),
          h(DC.FormMessage));
      } });
    }
    return h(DC.Form, form,
      h("form", { className: "grid max-w-md gap-5", onSubmit: form.handleSubmit(function () {}) },
        field("name", "Campaign name", "Only your team sees this name."),
        field("sender", "Sender mailbox", "Replies land in this inbox.", "email"),
        h("div", null, h(DC.Button, { type: "submit" }, "Save campaign"))));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in Forms

- [Calendar](/design/components/calendar.md): A month grid (react-day-picker 10) in the app's tokens: today and the selection in `primary`, outside days muted.
- [Checkbox](/design/components/checkbox.md): A 16px square check for independent on/off choices and row selection; the box fills in 150ms, then the tick draws in over 350ms (`.t-check`).
- [DateTimePicker](/design/components/date-time-picker.md): A date-and-time field in the app's own widget, instead of the browser's `datetime-local`; works in ISO strings.
- [Input](/design/components/input.md): The single-line text field: 36px tall, `input` border, `radius-md`, transparent over its ground (dark themes fill it with `input` at 30%).
- [Label](/design/components/label.md): The accessible field label (Radix Label): 14px medium, `leading-none`, 8px gap for an inline icon or `HelpTooltip`.
- [PhoneInput](/design/components/phone-input.md): An international phone field: `react-phone-number-input` with Max's `Input` and a searchable country picker that shows flags.
- [RadioGroup](/design/components/radio-group.md): One choice from a short, visible list (Radix RadioGroup): 16px rings in `primary`, 10px dot.
- [SecretInput](/design/components/secret-input.md): A field for API keys and tokens: masked by default with a reveal toggle, monospace, opted out of autofill.
- [Select](/design/components/select.md): A native-feeling single choice from a list (Radix Select): 36px trigger (`size="sm"`: 32px) with a chevron; the list opens in 250ms from 97% (`.t-dropdown`).
- [Slider](/design/components/slider.md): A Radix slider for picking a number, or a range with two thumbs, on a continuous scale.
- [Switch](/design/components/switch.md): The on/off control for settings that apply immediately (Radix Switch): a 32 × 18px pill, `input` track off, `primary` on, 16px thumb with a double-bounce (`.t-toggle`, 350ms thumb, 150ms track).
- [Textarea](/design/components/textarea.md): The multi-line field; it grows with its content (`field-sizing: content`) from a 64px minimum.
- [TimezoneSelector](/design/components/timezone-selector.md): A searchable combobox (a `Popover` holding a `Command` list) over every IANA time zone the browser knows.
- [TypedOptionPicker](/design/components/typed-option-picker.md): A searchable, grouped picker for long option lists (Popover + Command), in single and multi versions.
