

`FormBuilder.Bulk` reuses the same schema and `formDefs` as `FormBuilder.Single`, then adds CSV import, header mapping, a grid editor, validation, template download, and invalid-row export.

<Preview name="BulkBasicExample" />

## Usage [#usage]

```tsx
import { z } from "zod";
import {
  FormBuilder,
  type FormDefs,
} from "@tilt-legal/cubitt-components/form-builder";
```

```tsx
const memberSchema = z.object({
  firstName: z.string().min(1, "Required"),
  email: z.string().email(),
  role: z.enum(["admin", "member"]).default("member"),
  active: z.boolean().default(true),
});

const memberDefs = [
  {
    kind: "field",
    name: "firstName",
    label: "First name",
    component: "text",
    size: "half",
  },
  {
    kind: "field",
    name: "email",
    label: "Email",
    component: "email",
    size: "full",
  },
  {
    kind: "field",
    name: "role",
    label: "Role",
    component: "select",
    options: [
      { value: "admin", label: "Admin" },
      { value: "member", label: "Member" },
    ],
    size: "full",
  },
  {
    kind: "field",
    name: "active",
    label: "Active",
    component: "switch",
    size: "full",
  },
] as const satisfies FormDefs;
```

```tsx
<FormBuilder.Bulk
  formDefs={memberDefs}
  onSubmit={async ({ rows }) => {
    await importMembers(rows);
  }}
  schema={memberSchema}
  template={{ filename: "members.csv" }}
/>
```

## Examples [#examples]

### Basic Import [#basic-import]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="BulkBasicExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    <FormBuilder.Bulk
      formDefs={memberDefs}
      onSubmit={handleImport}
      schema={memberSchema}
      template={{ filename: "members.csv" }}
    />
    ```
  </Tab>
</Tabs>

## CSV Flow [#csv-flow]

1. Drop, choose, or paste CSV data.
2. Headers are mapped to fields by name, label, and aliases.
3. Raw CSV rows are shaped into schema values.
4. Rows render in a table using Cubitt form fields.
5. Zod validation populates inline row errors.
6. Submit receives `{ rows }`.

## Mapping Aliases [#mapping-aliases]

```tsx
<FormBuilder.Bulk
  formDefs={memberDefs}
  mapping={{
    headerAliases: {
      firstName: ["given_name", "first"],
      active: ["enabled", "status"],
    },
  }}
  onSubmit={handleImport}
  schema={memberSchema}
/>
```

## Template Downloads [#template-downloads]

```tsx
<FormBuilder.Bulk
  formDefs={memberDefs}
  onSubmit={handleImport}
  schema={memberSchema}
  template={{
    filename: "members-template.csv",
    exampleRow: {
      firstName: "Jane",
      email: "jane@example.com",
      role: "member",
      active: true,
    },
  }}
/>
```

## Controlled Rows [#controlled-rows]

```tsx
const [rows, setRows] = useState<Member[]>([]);

<FormBuilder.Bulk
  formDefs={memberDefs}
  onRowsChange={setRows}
  onSubmit={handleImport}
  rows={rows}
  schema={memberSchema}
/>
```

## API Reference [#api-reference]

| Prop                     | Type                                                   | Default  | Description                                                                     |
| ------------------------ | ------------------------------------------------------ | -------- | ------------------------------------------------------------------------------- |
| `schema`                 | `z.ZodTypeAny`                                         | -        | Zod schema applied to each row                                                  |
| `formDefs`               | `FormDefs<T>`                                          | -        | Shared definitions for columns, labels, field components, and CSV templates     |
| `onSubmit`               | `(opts: { rows: T[] }) => BulkSubmitResult<T>`         | -        | Submit handler. Return a `BulkSubmitFailure` to display row or form errors      |
| `onSubmitInvalid`        | `() => void`                                           | -        | Called when the default toolbar submit button is pressed while rows are invalid |
| `parseOptions`           | `{ delimiter?, headerRow?, maxRows?, localeDecimal? }` | -        | CSV parser configuration                                                        |
| `mapping`                | `{ headerAliases?: Record<string, string[]> }`         | -        | Header aliases for auto-mapping                                                 |
| `template`               | `{ filename?, exampleRow? }`                           | -        | Template download filename and optional example row                             |
| `accept`                 | `string`                                               | `".csv"` | File picker accept string                                                       |
| `validateOnEdit`         | `boolean`                                              | `true`   | Revalidate rows while editing                                                   |
| `rows`                   | `T[]`                                                  | -        | Controlled rows                                                                 |
| `onRowsChange`           | `(rows: T[]) => void`                                  | -        | Row change callback                                                             |
| `disabled`               | `boolean`                                              | `false`  | Disable ingest, editing, mapping, and actions                                   |
| `className`              | `string`                                               | -        | Layout class for the root form                                                  |
| `id`                     | `string`                                               | -        | Form id for external submit buttons                                             |
| `submitRef`              | `Ref<() => void>`                                      | -        | Imperative submit function                                                      |
| `toolbar`                | `false \| RenderToolbar`                               | -        | Hide, replace, or extend the toolbar                                            |
| `enableExportInvalidCsv` | `boolean`                                              | `true`   | Show the invalid-row export action                                              |

## Toolbar Hook [#toolbar-hook]

`FormBuilder.Bulk.useToolbar()` returns a handle for external toolbar composition.

| Property                   | Type                  | Description                |
| -------------------------- | --------------------- | -------------------------- |
| `actions.uploadFile`       | `() => void`          | Open the file picker       |
| `actions.reviewMapping`    | `() => void`          | Open the mapping dialog    |
| `actions.downloadTemplate` | `() => void`          | Download the CSV template  |
| `actions.validateAll`      | `() => void`          | Validate all rows          |
| `actions.submit`           | `() => void`          | Submit the form            |
| `actions.exportInvalidCsv` | `() => void`          | Export invalid rows        |
| `render`                   | `(slot) => ReactNode` | Pass to the `toolbar` prop |

## Error Return [#error-return]

```tsx
type BulkSubmitFailure = {
  error?: ZodError;
  formErrors?: string[];
  rows?: Array<{
    rowIndex: number;
    fieldErrors: Record<string, string[]>;
    rowErrors: string[];
  }>;
};
```
