# Form System

Type-safe, composable form handling built on [TanStack Form](https://tanstack.com/form) + shadcn/ui. Supports simple CRUD forms, multi-step wizards, sheet/dialog forms, dynamic arrays, nested objects, async validation, linked fields, and cross-field validation.

---

## Table of Contents

- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Usage Patterns](#usage-patterns)
  - [Pattern 1: useFormFields (recommended)](#pattern-1-useformfields--type-safe-flat-fields-recommended)
  - [Pattern 2: form.AppField render prop](#pattern-2-formappfield-render-prop--full-control)
  - [Pattern 3: Direct import](#pattern-3-direct-import--no-type-safety-zero-boilerplate)
  - [When to use which](#when-to-use-which)
- [Available Field Components](#available-field-components)
- [Validation](#validation)
  - [Recommended strategy](#recommended-strategy-field-level--form-level)
  - [Validator timing](#validator-timing)
  - [Zod schemas vs functions](#zod-schemas-vs-functions)
  - [Async validation](#async-validation)
  - [Linked / dependent field validation](#linked--dependent-field-validation)
  - [Cross-field (form-level) validation](#cross-field-form-level-validation)
  - [Error visibility](#error-visibility)
- [Listeners (Side Effects)](#listeners-side-effects)
- [Form Recipes](#form-recipes)
  - [Simple CRUD form](#simple-crud-form)
  - [Form in a Sheet or Dialog](#form-in-a-sheet-or-dialog)
  - [Multi-step wizard](#multi-step-wizard)
  - [Nested object fields](#nested-object-fields)
  - [Dynamic array rows](#dynamic-array-rows)
  - [Dependent dropdowns (country → state)](#dependent-dropdowns-country--state)
  - [Password confirmation (linked fields)](#password-confirmation-linked-fields)
- [Production Utilities](#production-utilities)
  - [FormErrors — form-level error display](#formerrors--form-level-error-display)
  - [scrollToFirstError — auto-scroll on failed submit](#scrolltofirsterror--auto-scroll-on-failed-submit)
- [Adding a New Field Type](#adding-a-new-field-type)
- [Type Safety Reference](#type-safety-reference)
- [Exports Reference](#exports-reference)
- [Dashboard Examples](#dashboard-examples)

---

## Architecture

```
form-context.tsx             fields/*.tsx
(contexts, structural        (TextField, FormTextField,
 components, createFormField, SelectField, FormSelectField,
 FieldConfig types,           ... base + composed exports)
 typedField, FormErrors,            │
 scrollToFirstError)                │
      ▲                             │
      │                             │
      └─────── tanstack-form.tsx ───┘
               (useAppForm, useFormFields,
                Form, SubmitButton, StepButton,
                withForm, withFieldGroup)
```

**Dependency rule:** `fields/*.tsx` imports from `form-context.tsx`. `tanstack-form.tsx` imports from both. Neither `form-context.tsx` nor `fields/*.tsx` imports from `tanstack-form.tsx` — no circular dependencies.

**Key files:**

| File                                    | What it provides                                                                                                                                                                                                 |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/components/ui/form-context.tsx`    | Shared primitives — contexts, `useFieldContext`, structural components (`FormFieldSet`, `FormField`, `FormFieldError`), `createFormField`, `FieldConfig` types, `typedField`, `FormErrors`, `scrollToFirstError` |
| `src/components/ui/tanstack-form.tsx`   | Main entry point — `useAppForm`, `useFormFields`, `Form`, `SubmitButton`, `StepButton`, `withForm`, `withFieldGroup`                                                                                             |
| `src/components/forms/fields/*.tsx`     | 8 field components, each exporting a base (`TextField`) and composed (`FormTextField`) variant                                                                                                                   |
| `src/components/forms/fields/index.tsx` | Barrel re-exports for all fields                                                                                                                                                                                 |

---

## File Structure (per feature)

Every form feature should split into **schema**, **constants**, and **component**:

```
src/features/products/
├── schemas/
│   └── product.ts              ← Zod schema + inferred FormValues type
├── constants/
│   └── product-options.ts      ← Select options, enums, static data
├── components/
│   ├── product-form.tsx         ← Form UI (imports schema + options)
│   └── product-form-fields.tsx  ← Optional: sections for large forms
```

**Why split?**

| Concern     | File                           | Benefit                                                                         |
| ----------- | ------------------------------ | ------------------------------------------------------------------------------- |
| **Schema**  | `schemas/product.ts`           | Reusable in API routes, server actions, data tables, tests — no `'use client'`  |
| **Type**    | `schemas/product.ts`           | `ProductFormValues` used in form, API, list components — single source of truth |
| **Options** | `constants/product-options.ts` | Shared between form selects, table filters, search facets                       |
| **Form UI** | `components/product-form.tsx`  | Pure UI — opens clean, no validation logic clutter                              |

**Schema file example:**

```ts
// src/features/products/schemas/product.ts
import * as z from 'zod';

export const productSchema = z.object({
  name: z.string().min(2, 'Product name must be at least 2 characters.'),
  category: z.string().min(1, 'Please select a category'),
  price: z.number({ message: 'Price is required' }),
  description: z.string().min(10, 'Description must be at least 10 characters.')
});

// Always prefer z.infer — guarantees the type matches the schema exactly.
// Manual types drift when the schema has unions, optionals, or refinements.
export type ProductFormValues = z.infer<typeof productSchema>;
```

> **Rule of thumb:** Use `z.infer<typeof schema>` as the form values type. Only override individual fields (via `Omit & { ... }`) when the form's runtime value shape genuinely differs from the schema output (e.g., a `File[]` field stored as `string` after upload).

**Form component imports the schema:**

```tsx
// src/features/products/components/product-form.tsx
import { productSchema, type ProductFormValues } from '@/features/products/schemas/product';
import { categoryOptions } from '@/features/products/constants/product-options';

const form = useAppForm({
  defaultValues: { ... } as ProductFormValues,
  validators: { onSubmit: productSchema },
  ...
});

const { FormTextField, FormSelectField } = useFormFields<ProductFormValues>();
```

**Same schema reused in API route:**

```ts
// src/app/api/products/route.ts
import { productSchema } from '@/features/products/schemas/product';

export async function POST(req: Request) {
  const body = await req.json();
  const data = productSchema.parse(body);  // same validation, zero duplication
  ...
}
```

### When a form grows large

For forms with 15+ fields, split the UI into section components:

```
components/
├── product-form.tsx              ← Main form (useAppForm, layout, submit)
├── product-basic-fields.tsx      ← Section: name, category, price
├── product-media-fields.tsx      ← Section: image upload
└── product-detail-fields.tsx     ← Section: description, tags, metadata
```

Each section receives the typed fields from `useFormFields` via props or calls `useFormFields` itself.

---

## Quick Start

```tsx
'use client';

import { useAppForm, useFormFields } from '@/components/ui/tanstack-form';
import * as z from 'zod';

const schema = z.object({
  name: z.string().min(2, 'Name is required'),
  email: z.string().email('Invalid email')
});

type FormValues = z.infer<typeof schema>;

export default function MyForm() {
  const form = useAppForm({
    defaultValues: { name: '', email: '' } as FormValues,
    validators: { onSubmit: schema },
    onSubmit: ({ value }) => console.log(value)
  });

  const { FormTextField } = useFormFields<FormValues>();

  return (
    <form.AppForm>
      <form.Form>
        <FormTextField
          name='name'
          label='Name'
          required
          validators={{ onBlur: z.string().min(2, 'Name is required') }}
        />
        <FormTextField
          name='email'
          label='Email'
          required
          type='email'
          validators={{ onBlur: z.string().email('Invalid email') }}
        />
        <form.SubmitButton label='Save' />
      </form.Form>
    </form.AppForm>
  );
}
```

---

## Usage Patterns

### Pattern 1: `useFormFields` — Type-safe flat fields (recommended)

Type-safe field names with autocomplete. Concise. Supports validators, listeners, `mode`, `defaultValue`. **Use for most forms.**

```tsx
import { useAppForm, useFormFields } from '@/components/ui/tanstack-form';

type FormValues = { name: string; email: string; category: string };

const form = useAppForm({
  defaultValues: { name: '', email: '', category: '' } as FormValues,
  validators: { onSubmit: schema },
  onSubmit: ({ value }) => { ... },
});

const { FormTextField, FormSelectField } = useFormFields<FormValues>();

<FormTextField name="email" label="Email" required />     // ✅ autocomplete
<FormTextField name="typo" label="Oops" />                // ❌ TypeScript error
```

**Props available on every `FormXxxField`:**

| Prop               | Type                                            | Description                                                                 |
| ------------------ | ----------------------------------------------- | --------------------------------------------------------------------------- |
| `name`             | `DeepKeys<T>` (via `useFormFields`) or `string` | Field path                                                                  |
| `validators`       | `FieldValidatorConfig`                          | `onBlur`, `onChange`, `onChangeAsync`, `onSubmit`, `onChangeListenTo`, etc. |
| `asyncDebounceMs`  | `number`                                        | Default debounce for all async validators                                   |
| `listeners`        | `FieldListenerConfig`                           | `onChange`, `onBlur`, `onMount`, `onSubmit` + debounce options              |
| `mode`             | `'value' \| 'array'`                            | Set to `'array'` for array fields                                           |
| `defaultValue`     | `unknown`                                       | Initial value (for dynamically added fields)                                |
| ...component props | varies                                          | `label`, `required`, `placeholder`, `options`, etc.                         |

### Pattern 2: `form.AppField` render prop — Full control

Type-safe names (native TanStack Form). Full field API access. **Use for custom fields, array fields, and any UI that doesn't fit a pre-built component.**

```tsx
<form.AppField
  name='framework' // ✅ type-safe
  validators={{ onBlur: z.string().min(1, 'Required') }}
>
  {(field) => (
    <field.FieldSet>
      <field.Field>
        <field.FieldLabel>Framework *</field.FieldLabel>
        <MyCombobox
          value={field.state.value}
          onChange={field.handleChange}
          onBlur={field.handleBlur}
        />
      </field.Field>
      <field.FieldError />
    </field.FieldSet>
  )}
</form.AppField>
```

**Components available inside the render prop (`field.XxxField`):**

| Component                | Purpose                                                     |
| ------------------------ | ----------------------------------------------------------- |
| `field.FieldSet`         | Wrapper — generates unique accessibility IDs                |
| `field.Field`            | Container — wires `aria-invalid`, `aria-describedby`        |
| `field.FieldLabel`       | `<label>` connected to the field                            |
| `field.FieldError`       | Renders validation errors (shows after touch or submit)     |
| `field.FieldContent`     | Flex container for label + description (horizontal layouts) |
| `field.FieldDescription` | Helper text below the field                                 |
| `field.TextField`        | Pre-built text input                                        |
| `field.TextareaField`    | Pre-built textarea                                          |
| `field.SelectField`      | Pre-built select                                            |
| `field.CheckboxField`    | Pre-built checkbox                                          |
| `field.SwitchField`      | Pre-built switch                                            |
| `field.RadioGroupField`  | Pre-built radio group                                       |
| `field.SliderField`      | Pre-built slider                                            |
| `field.FileUploadField`  | Pre-built file uploader                                     |
| `field.DatePickerField`  | Pre-built calendar date picker                              |

**Field API (`field.state`, `field.handleChange`, etc.):**

| Property/Method                       | Description                                 |
| ------------------------------------- | ------------------------------------------- |
| `field.state.value`                   | Current field value                         |
| `field.state.meta.isTouched`          | User has interacted                         |
| `field.state.meta.isDirty`            | Value differs from default                  |
| `field.state.meta.isValid`            | No validation errors                        |
| `field.state.meta.isValidating`       | Async validation in progress                |
| `field.state.meta.errors`             | Array of error messages                     |
| `field.handleChange(value)`           | Update field value                          |
| `field.handleBlur()`                  | Mark as touched + trigger onBlur validation |
| `field.pushValue(item)`               | Array mode: add item                        |
| `field.removeValue(index)`            | Array mode: remove item                     |
| `field.swapValues(a, b)`              | Array mode: swap items                      |
| `field.insertValue(index, item)`      | Array mode: insert at index                 |
| `field.form.setFieldValue(name, val)` | Set another field's value                   |
| `field.form.getFieldValue(name)`      | Read another field's value                  |

### Pattern 3: Direct import — No type safety, zero boilerplate

```tsx
import { FormTextField } from '@/components/forms/fields';

<FormTextField name='name' label='Name' />; // name is `string` — no type check
```

### When to use which

| Scenario                                           | Pattern       | Why                                         |
| -------------------------------------------------- | ------------- | ------------------------------------------- |
| Standard fields (text, select, checkbox, etc.)     | **Pattern 1** | Type-safe + concise                         |
| Custom one-off fields (date picker, OTP, combobox) | **Pattern 2** | Full field API access                       |
| Array fields with custom row layout                | **Pattern 2** | Need `pushValue`, `removeValue`, sub-fields |
| Array fields with composed component               | **Pattern 1** | Pass `mode="array"`                         |
| Multi-step form steps                              | **Pattern 2** | `group.AppField` + `field.TextField`        |
| Linked field validation (`onChangeListenTo`)       | **Pattern 2** | Need `fieldApi` in validator                |
| Quick prototype / dynamic field names              | **Pattern 3** | Fastest                                     |

---

## Available Field Components

Each field has two variants:

| Base (for render props) | Composed (for flat use) | Input type                                    |
| ----------------------- | ----------------------- | --------------------------------------------- |
| `TextField`             | `FormTextField`         | Text, email, password, tel, url, number       |
| `TextareaField`         | `FormTextareaField`     | Multi-line text with optional character count |
| `SelectField`           | `FormSelectField`       | Single-value dropdown (`options` prop)        |
| `CheckboxField`         | `FormCheckboxField`     | Boolean checkbox with label                   |
| `SwitchField`           | `FormSwitchField`       | Toggle switch with label + description        |
| `RadioGroupField`       | `FormRadioGroupField`   | Radio button group (`options` prop)           |
| `SliderField`           | `FormSliderField`       | Range slider with min/max display             |
| `FileUploadField`       | `FormFileUploadField`   | Drag-and-drop file upload                     |
| `DatePickerField`       | `FormDatePickerField`   | Calendar popover date picker                  |

**TextField** supports `type` prop: `'text'`, `'email'`, `'password'`, `'tel'`, `'url'`, `'number'`. Shows a spinner during async validation.

---

## Validation

### Recommended strategy: Field-level + Form-level

```
┌─────────────────────────────────────────────────────┐
│  onBlur (field-level)   → instant feedback on tab   │
│  onChangeAsync (field)  → server checks (debounced) │
│  onSubmit (form-level)  → catch-all safety net      │
└─────────────────────────────────────────────────────┘
```

### Validator timing

| Validator       | When it runs                | Use for                            |
| --------------- | --------------------------- | ---------------------------------- |
| `onChange`      | Every keystroke             | Instant feedback (use sparingly)   |
| `onBlur`        | When field loses focus      | Required checks, format validation |
| `onChangeAsync` | After debounce on keystroke | Server-side uniqueness checks      |
| `onBlurAsync`   | After debounce on blur      | Expensive server validation        |
| `onSubmit`      | On form submission          | Final catch-all                    |
| `onMount`       | When field mounts           | Pre-validation                     |

### Zod schemas vs functions

```tsx
// Zod schema — StandardSchemaV1, no adapter needed (Zod v4)
validators={{ onBlur: z.string().email('Invalid email') }}

// Sync function — return error string or undefined
validators={{
  onChange: ({ value }) => value.length < 3 ? 'Too short' : undefined,
}}

// Async function — supports AbortSignal for cancellation
validators={{
  onChangeAsync: async ({ value, signal }) => {
    const res = await fetch(`/api/check?q=${value}`, { signal });
    const { ok } = await res.json();
    return ok ? undefined : 'Already taken';
  },
  onChangeAsyncDebounceMs: 500,
}}
```

### Async validation

```tsx
<FormTextField
  name='username'
  label='Username'
  validators={{
    onBlur: z.string().min(3, 'Too short'),
    onChangeAsync: async ({ value }: { value: string }) => {
      if (!value || value.length < 3) return undefined;
      await new Promise((r) => setTimeout(r, 500)); // simulated API
      if (value === 'admin') return 'Username is taken';
      return undefined;
    },
    onChangeAsyncDebounceMs: 500
  }}
/>
```

`TextField` automatically shows a spinner when `isValidating` is true.

### Linked / dependent field validation

Use `onChangeListenTo` to re-run validation when another field changes:

```tsx
<form.AppField
  name='confirmPassword'
  validators={{
    onChangeListenTo: ['password'],
    onChange: ({ value, fieldApi }) => {
      const password = fieldApi.form.getFieldValue('password');
      return value !== password ? 'Passwords do not match' : undefined;
    }
  }}
>
  {(field) => <field.TextField label='Confirm Password' type='password' />}
</form.AppField>
```

### Cross-field (form-level) validation

For validation that spans multiple fields, use form-level validators:

```tsx
const form = useAppForm({
  defaultValues: { ... },
  validators: {
    onSubmit: fullZodSchema,  // validates entire form shape
    // or use a function:
    onChange: ({ value }) => {
      if (value.startDate > value.endDate) return 'End date must be after start';
      return undefined;
    },
  },
});
```

Form-level errors are rendered by `<FormErrors />`.

### Error visibility

Errors are shown when either condition is met:

1. **Field is touched** — user has interacted with the field (blur/change)
2. **Form has been submitted** — at least one submit attempt, even if fields weren't touched

This prevents showing errors on a pristine form while ensuring all errors appear after submit.

---

## Listeners (Side Effects)

Listeners run side effects without affecting validation. Use them to reset dependent fields, sync values, trigger analytics, etc.

```tsx
<FormSelectField
  name='country'
  label='Country'
  options={countries}
  listeners={{
    onChange: ({ value, fieldApi }) => {
      fieldApi.form.setFieldValue('state', '');
      fieldApi.form.setFieldValue('city', '');
    }
  }}
/>
```

| Listener   | When it fires             |
| ---------- | ------------------------- |
| `onChange` | After field value changes |
| `onBlur`   | When field loses focus    |
| `onMount`  | When field mounts         |
| `onSubmit` | On form submission        |

Each has an optional `*DebounceMs` companion (e.g., `onChangeDebounceMs: 300`).

---

## Form Recipes

### Simple CRUD form

```tsx
const form = useAppForm({
  defaultValues: { name: '', email: '' } as FormValues,
  validators: { onSubmit: schema },
  onSubmit: async ({ value }) => {
    await saveToApi(value);
    toast.success('Saved!');
  }
});

const { FormTextField } = useFormFields<FormValues>();

<form.AppForm>
  <form.Form>
    <FormTextField name='name' label='Name' required validators={{ onBlur: z.string().min(2) }} />
    <FormTextField
      name='email'
      label='Email'
      required
      type='email'
      validators={{ onBlur: z.string().email() }}
    />
    <form.SubmitButton label='Save' />
  </form.Form>
</form.AppForm>;
```

### Form in a Sheet or Dialog

Use the HTML `form` attribute to connect an external submit button:

```tsx
const [open, setOpen] = useState(false);

const form = useAppForm({
  defaultValues: { ... },
  onSubmit: ({ value }) => {
    toast.success('Created!');
    setOpen(false);
    form.reset();
  },
});

<Sheet open={open} onOpenChange={setOpen}>
  <SheetContent>
    <SheetHeader>...</SheetHeader>
    <ScrollArea className="flex-1">
      <form.AppForm>
        <form.Form id="sheet-form" className="space-y-4 p-0 md:p-0">
          {/* fields */}
        </form.Form>
      </form.AppForm>
    </ScrollArea>
    <SheetFooter>
      <Button type="submit" form="sheet-form">Save</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>
```

Key: `id="sheet-form"` on `form.Form` + `form="sheet-form"` on the external button.

### Multi-step wizard

```tsx
import { useAppForm, withFieldGroup } from '@/components/ui/tanstack-form';
import { revalidateLogic, useStore } from '@tanstack/react-form';

// 1. Define step groups
const Step1 = withFieldGroup({
  defaultValues: { name: '', category: '' },
  render: ({ group }) => (
    <>
      <group.AppField name="name">
        {(field) => <field.TextField label="Name" required />}
      </group.AppField>
      <group.AppField name="category">
        {(field) => <field.SelectField label="Category" options={opts} />}
      </group.AppField>
    </>
  ),
});

// 2. Define per-step schemas
const stepSchemas = [
  fullSchema.pick({ name: true, category: true }),
  fullSchema.pick({ description: true }),
  z.object({}),  // review step — no validation
];

// 3. Create form with dynamic validation
const { currentValidator, currentStep, ... } = useFormStepper(stepSchemas);

const form = useAppForm({
  defaultValues: { ... },
  validationLogic: revalidateLogic(),
  validators: {
    onDynamic: currentValidator,
    onDynamicAsyncDebounceMs: 500,
  },
  onSubmit: ({ value }) => { ... },
});

// 4. Render current step
<Step1 form={form} fields={{ name: 'name', category: 'category' }} />
```

See `src/features/forms/components/multi-step-product-form.tsx`.

### Nested object fields

Use dot-notation for nested paths. `DeepKeys<T>` provides autocomplete for `team.name`, `team.size`, etc.

```tsx
type FormValues = {
  team: { name: string; size: number };
};

const { FormTextField } = useFormFields<FormValues>();

<FormTextField name="team.name" label="Team Name" required />
<FormTextField name="team.size" label="Team Size" type="number" />
```

### Dynamic array rows

Use `form.AppField` with `mode="array"` for full control over add/remove:

```tsx
type FormValues = {
  members: Array<{ name: string; role: string }>;
};

<form.AppField name='members' mode='array'>
  {(field) => (
    <div className='space-y-3'>
      {field.state.value.map((_, i) => (
        <div key={i} className='flex gap-2'>
          <form.AppField
            name={`members[${i}].name`}
            validators={{ onBlur: z.string().min(1, 'Required') }}
          >
            {(sub) => (
              <sub.FieldSet className='flex-1'>
                <sub.Field>
                  <Input
                    placeholder='Name'
                    value={sub.state.value}
                    onChange={(e) => sub.handleChange(e.target.value)}
                    onBlur={sub.handleBlur}
                  />
                </sub.Field>
                <sub.FieldError />
              </sub.FieldSet>
            )}
          </form.AppField>
          <Button variant='ghost' size='icon' onClick={() => field.removeValue(i)}>
            <Icons.close className='h-4 w-4' />
          </Button>
        </div>
      ))}
      <Button variant='outline' size='sm' onClick={() => field.pushValue({ name: '', role: '' })}>
        Add Member
      </Button>
    </div>
  )}
</form.AppField>;
```

Array methods: `pushValue`, `removeValue`, `insertValue`, `replaceValue`, `swapValues`, `moveValue`.

### Dependent dropdowns (country → state)

Combine `listeners` with reactive `useStore`:

```tsx
// Read country value reactively
const selectedCountry = useStore(form.store, (s) => s.values.country);
const stateOptions = countryStateMap[selectedCountry] ?? [];

<FormSelectField
  name="country"
  label="Country"
  options={countryOptions}
  listeners={{
    onChange: ({ fieldApi }) => {
      fieldApi.form.setFieldValue('state', '');  // reset dependent field
    },
  }}
/>
<FormSelectField
  name="state"
  label="State"
  options={stateOptions}
  placeholder={selectedCountry ? 'Select state' : 'Select a country first'}
/>
```

### Password confirmation (linked fields)

```tsx
<form.AppField
  name='confirmPassword'
  validators={{
    onChangeListenTo: ['password'],
    onChange: ({ value, fieldApi }) => {
      const password = fieldApi.form.getFieldValue('password');
      return value !== password ? 'Passwords do not match' : undefined;
    },
    onBlur: z.string().min(1, 'Required')
  }}
>
  {(field) => <field.TextField label='Confirm Password' required type='password' />}
</form.AppField>
```

### Checkbox group (multi-select array)

For selecting multiple values from a list, use `form.AppField` with `mode="array"`:

```tsx
const positionOptions = [
  { value: 'frontend', label: 'Frontend Developer' },
  { value: 'backend', label: 'Backend Developer' },
  { value: 'fullstack', label: 'Full Stack Developer' }
];

<form.AppField name='position' mode='array'>
  {(field) => {
    const values: string[] = field.state.value || [];
    return (
      <field.FieldSet>
        <FieldLabel>Position(s) *</FieldLabel>
        <div className='grid grid-cols-2 gap-3'>
          {positionOptions.map((opt) => (
            <div key={opt.value} className='flex items-center space-x-2'>
              <Checkbox
                id={`position-${opt.value}`}
                checked={values.includes(opt.value)}
                onCheckedChange={(checked) => {
                  if (checked) field.pushValue(opt.value);
                  else {
                    const idx = values.indexOf(opt.value);
                    if (idx > -1) field.removeValue(idx);
                  }
                }}
              />
              <Label htmlFor={`position-${opt.value}`}>{opt.label}</Label>
            </div>
          ))}
        </div>
        <field.FieldError />
      </field.FieldSet>
    );
  }}
</form.AppField>;
```

### Date picker field (Calendar popover)

For date selection, use `form.AppField` with a Calendar popover. Store as ISO string:

```tsx
<form.AppField
  name='available-date'
  validators={{ onBlur: z.string().min(1, 'Please select a date') }}
>
  {(field) => (
    <field.FieldSet>
      <field.Field>
        <field.FieldLabel>Available Date *</field.FieldLabel>
        <Popover>
          <PopoverTrigger asChild>
            <Button
              variant='outline'
              className={cn(
                'w-full justify-start text-left font-normal',
                !field.state.value && 'text-muted-foreground'
              )}
            >
              <Icons.calendar className='mr-2 h-4 w-4' />
              {field.state.value ? format(new Date(field.state.value), 'PPP') : 'Pick a date'}
            </Button>
          </PopoverTrigger>
          <PopoverContent className='w-auto p-0' align='start'>
            <Calendar
              mode='single'
              selected={field.state.value ? new Date(field.state.value) : undefined}
              onSelect={(date) => {
                field.handleChange(date ? date.toISOString() : '');
                field.handleBlur();
              }}
              disabled={(date) => date < new Date()}
            />
          </PopoverContent>
        </Popover>
      </field.Field>
      <field.FieldError />
    </field.FieldSet>
  )}
</form.AppField>
```

### Real-world example: Job Application form

A complete form combining flat fields, checkbox group, date picker, select, file upload, and production utilities. Follows the split-file pattern:

```
src/features/applications/
├── schemas/application.ts         ← Zod schema + z.infer type
├── constants/application-options.ts ← Position & experience options
├── components/application-form.tsx  ← Form UI
```

**Schema** (`schemas/application.ts`):

```ts
export const applicationSchema = z.object({
  firstName: z.string({ error: 'This field is required' }),
  lastName: z.string({ error: 'This field is required' }),
  email: z.email({ error: 'Please enter a valid email' }),
  'github-url': z.url({ error: 'Please enter a valid url' }).optional(),
  'linkedin-url': z.url({ error: 'Please enter a valid url' }).optional(),
  position: z.array(z.string()).min(1, 'Please select at least one item'),
  experience: z.string().min(1, 'Please select an item'),
  'available-date': z.string().min(1, 'Please select a date'),
  'cover-letter': z.string().optional(),
  'file-upload': z.union([z.file(), z.array(z.file()).nonempty(), ...]).optional(),
});

export type ApplicationFormValues = z.infer<typeof applicationSchema>;
```

**Form** (`components/application-form.tsx`):

```tsx
import { applicationSchema, type ApplicationFormValues } from '../schemas/application';
import { positionOptions, experienceOptions } from '../constants/application-options';

const form = useAppForm({
  defaultValues: { ... } as ApplicationFormValues,
  validators: { onSubmit: applicationSchema },
  onSubmitInvalid: () => scrollToFirstError(),
  onSubmit: ({ value }) => { ... },
});

const { FormTextField, FormSelectField, FormTextareaField, FormFileUploadField } =
  useFormFields<ApplicationFormValues>();

// Flat fields for text/email/url/select/textarea/file
<FormTextField name="firstName" label="First Name" required ... />

// AppField for checkbox group (position) and date picker (available-date)
<form.AppField name="position" mode="array">...</form.AppField>
<form.AppField name="available-date">...</form.AppField>
```

See `/dashboard/forms/application` for the full working example.

---

## Production Utilities

### FormErrors — form-level error display

Renders errors from form-level validators (cross-field validation). Place at the top of the form.

```tsx
import { FormErrors } from '@/components/ui/tanstack-form';

<form.AppForm>
  <form.Form>
    <FormErrors />
    {/* fields */}
  </form.Form>
</form.AppForm>;
```

### scrollToFirstError — auto-scroll on failed submit

Scrolls to and focuses the first field with a validation error. Wire it to `onSubmitInvalid`:

```tsx
import { scrollToFirstError } from '@/components/ui/tanstack-form';

const form = useAppForm({
  ...
  onSubmitInvalid: () => scrollToFirstError(),
});
```

---

## Adding a New Field Type

Creating a new field (e.g., `DatePickerField`) requires **2 touchpoints**:

### 1. Create the field file

```tsx
// src/components/forms/fields/date-picker-field.tsx
'use client';

import { useStore } from '@tanstack/react-form';
import { FieldLabel } from '@/components/ui/field';
import {
  useFieldContext,
  FormFieldSet,
  FormField,
  FormFieldError,
  createFormField
} from '@/components/ui/form-context';

interface DatePickerFieldProps {
  label: string;
  required?: boolean;
}

export function DatePickerField({ label, required }: DatePickerFieldProps) {
  const field = useFieldContext();
  const value = useStore(field.store, (s) => s.value) as Date | undefined;

  return (
    <FormFieldSet>
      <FormField>
        <FieldLabel>
          {label}
          {required && ' *'}
        </FieldLabel>
        {/* Your date picker UI here — call field.handleChange and field.handleBlur */}
      </FormField>
      <FormFieldError />
    </FormFieldSet>
  );
}

export const FormDatePickerField = createFormField(DatePickerField);
```

### 2. Export from barrel

```tsx
// src/components/forms/fields/index.tsx
export { DatePickerField } from './date-picker-field';
export { FormDatePickerField } from './date-picker-field';
```

### 3. Use it

```tsx
// Direct import (Pattern 3)
import { FormDatePickerField } from '@/components/forms/fields';
<FormDatePickerField name='birthDate' label='Birth Date' />;

// Type-safe with typedField (Pattern 1 for custom fields)
import { typedField } from '@/components/ui/tanstack-form';
const narrow = typedField<FormValues>();
const TypedDatePicker = narrow(FormDatePickerField);
<TypedDatePicker name='birthDate' label='Birth Date' />;

// Or add to useFormFields in tanstack-form.tsx for built-in support
```

### Optional: Register for AppField render props

To use `field.DatePickerField` inside `form.AppField`, add to `fieldComponents` in `tanstack-form.tsx`.

To include in `useFormFields`, add to its return object.

---

## Type Safety Reference

| What                                          | Type-safe? | How                                           |
| --------------------------------------------- | :--------: | --------------------------------------------- |
| Field names via `useFormFields<T>()`          |    Yes     | `DeepKeys<T>` narrows `name`                  |
| Field names via `form.AppField`               |    Yes     | Native TanStack Form typing                   |
| Field names via `typedField<T>()(Component)`  |    Yes     | `DeepKeys<T>` narrowing for custom fields     |
| Field names via direct `FormTextField` import |     No     | `name` is `string`                            |
| Nested paths (`team.name`, `members[0].role`) |    Yes     | `DeepKeys<T>` resolves dot/bracket notation   |
| Validator values (Zod schema)                 |    Yes     | StandardSchemaV1 pass-through                 |
| Validator functions                           |  Partial   | `value` typed as `unknown` — cast in function |
| Listener callbacks                            |  Partial   | `value` typed as `unknown` — cast in callback |

---

## Exports Reference

### From `@/components/ui/tanstack-form`

| Export                 | Type      | Purpose                                                                  |
| ---------------------- | --------- | ------------------------------------------------------------------------ |
| `useAppForm`           | Hook      | Create a form instance                                                   |
| `useFormFields<T>()`   | Hook      | Get type-safe composed field components                                  |
| `withForm`             | HOC       | Wrap a component with form context                                       |
| `withFieldGroup`       | HOC       | Create multi-step field groups                                           |
| `useFormContext`       | Hook      | Access form instance from context                                        |
| `useFieldContext`      | Hook      | Access field API from context                                            |
| `createFormField`      | Utility   | Create a composed field from a base field                                |
| `typedField<T>()`      | Utility   | Narrow any composed field's name to `DeepKeys<T>`                        |
| `revalidateLogic`      | Utility   | Dynamic validation logic for multi-step                                  |
| `scrollToFirstError`   | Utility   | Scroll + focus first invalid field                                       |
| `FormFieldSet`         | Component | Structural — accessibility ID wrapper                                    |
| `FormField`            | Component | Structural — aria-invalid, aria-describedby                              |
| `FormFieldError`       | Component | Renders field-level errors                                               |
| `FormErrors`           | Component | Renders form-level errors                                                |
| `FieldConfig`          | Type      | `validators` + `asyncDebounceMs` + `listeners` + `mode` + `defaultValue` |
| `FieldValidatorConfig` | Type      | Validator timing options                                                 |
| `FieldListenerConfig`  | Type      | Listener options                                                         |
| `WithTypedName`        | Type      | Narrow component's `name` prop                                           |

### From `@/components/forms/fields`

| Base (render prop) | Composed (flat)       |
| ------------------ | --------------------- |
| `TextField`        | `FormTextField`       |
| `TextareaField`    | `FormTextareaField`   |
| `SelectField`      | `FormSelectField`     |
| `CheckboxField`    | `FormCheckboxField`   |
| `SwitchField`      | `FormSwitchField`     |
| `RadioGroupField`  | `FormRadioGroupField` |
| `SliderField`      | `FormSliderField`     |
| `FileUploadField`  | `FormFileUploadField` |
| `DatePickerField`  | `FormDatePickerField` |

---

## Dashboard Examples

### Form Pages (`/dashboard/forms/...`)

| Page                  | Route                         | Patterns demonstrated                                                                                                                                                                                                                           |
| --------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Basic Form**        | `/dashboard/forms/basic`      | All 8 field types, `useFormFields`, `onBlur` + async validation, listeners, form data preview                                                                                                                                                   |
| **Multi-Step Form**   | `/dashboard/forms/multi-step` | `withFieldGroup`, per-step Zod schemas, `revalidateLogic`, step navigation, review summary                                                                                                                                                      |
| **Sheet & Dialog**    | `/dashboard/forms/sheet-form` | Form in Sheet with external submit button, form in Dialog, close + reset on success                                                                                                                                                             |
| **Advanced Patterns** | `/dashboard/forms/advanced`   | Async validation (username check), linked fields (`onChangeListenTo` for password confirm), nested objects (`team.name`), dynamic array rows (members), dependent dropdowns (country → state with listener), `FormErrors`, `scrollToFirstError` |

### Other Forms

| Form          | File                                                   | Patterns                                   |
| ------------- | ------------------------------------------------------ | ------------------------------------------ |
| Product CRUD  | `src/features/products/components/product-form.tsx`    | Pattern 1, split schema, onBlur validators |
| Sheet Product | `src/features/forms/components/sheet-product-form.tsx` | Pattern 2 in Sheet                         |
| Auth          | `src/features/auth/components/user-auth-form.tsx`      | Pattern 2, minimal                         |
