

<Preview name="AttachmentPickerCompoundExample" />

## Overview [#overview]

`AttachmentPicker` is a compound component that provides a dialog for selecting items across one or more panels. It handles dialog layout, panel switching, per-panel selection state, and the confirm/cancel flow.

When only one panel is registered the dropdown switcher is replaced by plain text. If `onConfirm` returns a `Promise`, the picker enters a locked pending state (confirm spinner + non-interactive panels/toolbar) until the Promise settles.

```tsx
import { Button } from "@tilt-legal/cubitt-components/primitives";
import { AttachmentPicker } from "@tilt-legal/cubitt-components/composites";

<AttachmentPicker onConfirm={(selections) => { /* Record<panelId, string[]> */ }}>
  <AttachmentPicker.Trigger render={<Button variant="secondary" />}>
    Attach
  </AttachmentPicker.Trigger>
  <AttachmentPicker.Content>
    <AttachmentPicker.FilesPanel files={files} />
    <AttachmentPicker.InstructionsPanel instructions={instructions} />
  </AttachmentPicker.Content>
</AttachmentPicker>
```

## Confirm Flows [#confirm-flows]

`AttachmentPicker` supports two primary confirmation paths:

* **Pick-only (sync)** — `onConfirm` returns `void` and the dialog closes based on `autoClose`.
* **Create/submit (async)** — `onConfirm` returns a `Promise`; the picker locks interaction while pending. On resolve, it closes when `autoClose` is enabled. On reject, it stays open.

<Callout title="Golden Path" type="tip">
  Return a Promise from `onConfirm` and keep it pending until all downstream
  work you care about is complete (mutation success, cache refresh, row visible,
  status transition, etc). This keeps loading and closing behavior in one place.
  With non-URL usage, `autoClose` defaults to `true`, so the dialog closes after
  successful resolve.
</Callout>

```tsx
<AttachmentPicker
  onConfirm={async (selections) => {
    await createTranscription(selections.files);
    await refetchTranscriptions();
    await waitForRowToEnterProcessing();
  }}
/>
```

Use `confirmPending` only when pending state is managed outside `onConfirm`'s returned Promise (for example event-driven consumers or externally controlled workflows):

```tsx
const [creating, setCreating] = useState(false);

<AttachmentPicker
  autoClose={false}
  confirmPending={creating}
  onConfirm={(selections) => {
    setCreating(true);
    void createTranscription(selections.files).then(() => {
      // e.g. after cache refresh / row appears
      setCreating(false);
      setOpen(false);
    });
  }}
/>
```

## Panels [#panels]

### Files Panel [#files-panel]

The consumer provides an array of `FileAsset` objects — all UI is handled internally:

* **Search** — full-text filter by file name
* **Sort** — by name, size, or date (ascending/descending)
* **View toggle** — switch between list and grid
* **Matter filter** — combobox auto-derived from files with a `matters` property
* **Upload** — built-in upload button with MIME filtering and size validation (enabled when `onUploadClick` is passed). Pass `uploadProgress` to show real-time upload status, progress, and ETA per file
* **Selection** — single or multiple file selection via `selectionMode`

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

  <Tab value="Code">
    ```tsx
    <AttachmentPicker onConfirm={(s) => handleFiles(s.files)}>
      <AttachmentPicker.Trigger render={<Button variant="secondary" />}>
        Select Files
      </AttachmentPicker.Trigger>
      <AttachmentPicker.Content>
        <AttachmentPicker.FilesPanel
          files={files}
          selectionMode="single"
          onUploadClick={(files) => uploadFiles(files)}
          uploadProgress={uploadProgress}
          accept={["audio/*", "video/*"]}
          maxSizeMB={100}
        />
      </AttachmentPicker.Content>
    </AttachmentPicker>
    ```
  </Tab>
</Tabs>

### Instructions Panel [#instructions-panel]

Renders a searchable checkbox card grid. The consumer provides an array of `{ id, name, description }` objects — the panel handles search filtering, selection state, and layout.

Accepts `headerActions` for extra toolbar controls (e.g. a "Configure" button rendered next to the search).

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

  <Tab value="Code">
    ```tsx
    <AttachmentPicker onConfirm={(s) => handleInstructions(s.instructions)}>
      <AttachmentPicker.Trigger render={<Button variant="secondary" />}>
        Select Instructions
      </AttachmentPicker.Trigger>
      <AttachmentPicker.Content>
        <AttachmentPicker.InstructionsPanel
          instructions={instructions}
          headerActions={<Button onClick={onConfigure}>Configure</Button>}
        />
      </AttachmentPicker.Content>
    </AttachmentPicker>
    ```
  </Tab>
</Tabs>

### Custom Panel [#custom-panel]

Use `AttachmentPicker.Panel` when the pre-built panels don't fit. You provide the panel body, header toolbar, and optional footer — the dialog frame, panel switching, confirm/cancel flow, and selection aggregation are still handled by `AttachmentPicker`.

Selection within a custom panel is managed via `AttachmentPicker.usePanelSelection(panelId)`, which returns `{ selectedIds, setSelectedIds, toggleId, count }`. Selected IDs are included in the `onConfirm` callback alongside other panels.

```tsx
<AttachmentPicker.Panel
  id="templates"
  label="Templates"
  itemLabel="template"
  headerToolbar={<SearchExpand>...</SearchExpand>}
  footer={<Button variant="secondary">Manage Templates</Button>}
>
  <TemplateGrid />
</AttachmentPicker.Panel>
```

#### Using selection in a custom panel [#using-selection-in-a-custom-panel]

```tsx
function TemplateGrid() {
  const { selectedIds, toggleId } = AttachmentPicker.usePanelSelection("templates");

  return templates.map((t) => (
    <button key={t.id} onClick={() => toggleId(t.id)}>
      <Checkbox checked={selectedIds.includes(t.id)} />
      {t.name}
    </button>
  ));
}
```

## Virtual Files [#virtual-files]

The files panel supports [virtual files](/composites/files#virtual-files) for derived content like transcripts or AI summaries. Mix them into the same `files` array by using the `FileAsset` union contract:

* Standard row: `asset_type: "standard"` with `mime_type` and `size`
* Virtual row: `asset_type: "virtual"` with `virtual_type`

When processing `onConfirm` selections, use `asset_type` / `virtual_type` on the source row to branch behavior when needed:

```tsx
<AttachmentPicker
  onConfirm={(selections) => {
    const selectedIds = selections.files;
    const sourceById = new Map(files.map((file) => [file.id, file]));
    const getVirtualType = (id: string) =>
      sourceById.get(id)?.asset_type === "virtual"
        ? sourceById.get(id)?.virtual_type
        : undefined;

    const transcriptIds = selectedIds.filter(
      (id) => getVirtualType(id) === "transcript"
    );
    const realFileIds = selectedIds.filter((id) => !getVirtualType(id));
  }}
>
  <AttachmentPicker.Content>
    <AttachmentPicker.FilesPanel files={files} />
  </AttachmentPicker.Content>
</AttachmentPicker>
```

## Slots [#slots]

All panels accept `footer` and `headerActions` (or `headerToolbar` for custom panels) to inject consumer content.

Use `footerAlign` to position footer content on the left (next to the selection summary) or right (next to the confirm button). Use `showSelectionSummary={false}` to hide the "X items selected" text.

```tsx
<AttachmentPicker.FilesPanel
  files={files}
  footer={<ProviderSelector value={provider} onChange={setProvider} />}
  footerAlign="left"
  showSelectionSummary={false}
/>
```

## Examples [#examples]

### Create Transcription [#create-transcription]

A production-style dialog that combines single file selection, built-in upload with MIME filtering (`audio/*`, `video/*`), a left-aligned provider selector in the footer, custom title/confirm label, and async confirm with a locked pending state.

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

  <Tab value="Code">
    ```tsx
    import {
      Button,
      Select,
      SelectContent,
      SelectItem,
      SelectTrigger,
      SelectValue,
    } from "@tilt-legal/cubitt-components/primitives";
    import { AttachmentPicker, type UploadProgressState } from "@tilt-legal/cubitt-components/composites";

    const PROVIDERS = [
      { id: "deepgram", label: "Deepgram" },
      { id: "elevenlabs", label: "ElevenLabs" },
      { id: "azure", label: "Azure Speech" },
    ];
    const providerItems = PROVIDERS.map((provider) => ({
      label: provider.label,
      value: provider.id,
    }));

    function CreateTranscriptionDialog({ files, uploadProgress, onUpload }) {
      const [provider, setProvider] = useState("deepgram");

      return (
        <AttachmentPicker
          title="Select file"
          confirmLabel="Create"
          onConfirm={async (selections) => {
            await createTranscription(selections.files, provider);
          }}
        >
          <AttachmentPicker.Trigger render={<Button variant="secondary" />}>
            Create Transcription
          </AttachmentPicker.Trigger>

          <AttachmentPicker.Content>
            <AttachmentPicker.FilesPanel
              files={files}
              selectionMode="single"
              onUploadClick={onUpload}
              uploadProgress={uploadProgress}
              accept={["audio/*", "video/*"]}
              footerAlign="left"
              showSelectionSummary={false}
              footer={
                <Select
                  items={providerItems}
                  value={provider}
                  onValueChange={setProvider}
                >
                  <SelectTrigger className="w-40">
                    <SelectValue placeholder="Provider" />
                  </SelectTrigger>
                  <SelectContent>
                    {PROVIDERS.map((p) => (
                      <SelectItem key={p.id} value={p.id}>
                        {p.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              }
            />
          </AttachmentPicker.Content>
        </AttachmentPicker>
      );
    }
    ```
  </Tab>
</Tabs>

## API Reference [#api-reference]

### AttachmentPicker (root) [#attachmentpicker-root]

| Prop                | Type                                    | Default                                          | Description                                                                                               |
| ------------------- | --------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `open`              | `boolean`                               | —                                                | Controlled open state                                                                                     |
| `onOpenChange`      | `(open: boolean) => void`               | —                                                | Open state change callback                                                                                |
| `initialSelections` | `Record<string, string[]>`              | —                                                | Pre-selected IDs per panel (synced on open)                                                               |
| `onConfirm`         | `(selections) => void \| Promise<void>` | —                                                | Called on confirm. Return a Promise for the locked async path (`resolve` = success, `reject` = stay open) |
| `confirmPending`    | `boolean`                               | `false`                                          | Advanced escape hatch for pending managed outside `onConfirm`'s Promise                                   |
| `autoClose`         | `boolean`                               | `true` (no paramName) / `false` (with paramName) | Whether the dialog auto-closes after successful sync/async confirm                                        |
| `title`             | `string`                                | `"Attachments"` / `"Attach"`                     | Dialog title. Defaults to "Attachments" for single panel, "Attach" for multiple                           |
| `confirmLabel`      | `string`                                | `"Confirm"` / `"Attach"`                         | Confirm button label. Defaults to "Confirm" for single panel, "Attach" for multiple                       |
| `paramName`         | `string`                                | —                                                | URL parameter name for syncing dialog state with URL                                                      |
| `paramMatchValue`   | `string`                                | —                                                | Match this specific URL value to open                                                                     |

### AttachmentPicker.FilesPanel [#attachmentpickerfilespanel]

| Prop                   | Type                                        | Default      | Description                                                                                      |
| ---------------------- | ------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------ |
| `files`                | `FileAsset[]`                               | —            | Files to display                                                                                 |
| `uploadProgress`       | `UploadProgressState \| UploadProgressLike` | —            | Upload progress state. Files with matching IDs show upload status and progress                   |
| `defaultView`          | `"list" \| "grid"`                          | `"list"`     | Initial view mode                                                                                |
| `selectionMode`        | `"single" \| "multiple"`                    | `"multiple"` | Single or multi-file selection                                                                   |
| `panelId`              | `string`                                    | `"files"`    | Panel ID for selection state                                                                     |
| `label`                | `string`                                    | `"Files"`    | Panel switcher label                                                                             |
| `itemLabel`            | `string`                                    | `"file"`     | Singular noun for count                                                                          |
| `headerActions`        | `ReactNode`                                 | —            | Extra actions in the header toolbar                                                              |
| `footer`               | `ReactNode`                                 | —            | Extra footer content (e.g. provider selector)                                                    |
| `footerAlign`          | `"left" \| "right"`                         | `"right"`    | Where footer content is positioned                                                               |
| `showSelectionSummary` | `boolean`                                   | `true`       | Show "X items selected" text                                                                     |
| `onUploadClick`        | `(files: File[]) => void`                   | —            | Enables the built-in upload button                                                               |
| `accept`               | `string[]`                                  | –            | MIME patterns for upload (e.g. `["audio/*"]`). Required when `onUploadClick` is provided.        |
| `maxSizeMB`            | `number \| null`                            | `null`       | Max upload file size in MB                                                                       |
| `uploadDisabled`       | `boolean`                                   | `false`      | Disable the upload button                                                                        |
| `onCancelUpload`       | `(id: string) => void`                      | —            | Called when the user cancels an in-progress upload. Enables the cancel button on uploading files |
| `loading`              | `boolean`                                   | `false`      | Shows skeleton items and disables toolbar controls while files are loading                       |
| `defaultSearch`        | `string`                                    | `""`         | Default search query                                                                             |

### AttachmentPicker.InstructionsPanel [#attachmentpickerinstructionspanel]

| Prop                   | Type                          | Default          | Description                                                 |
| ---------------------- | ----------------------------- | ---------------- | ----------------------------------------------------------- |
| `instructions`         | `{ id, name, description }[]` | —                | Instructions to display                                     |
| `panelId`              | `string`                      | `"instructions"` | Panel ID for selection state                                |
| `label`                | `string`                      | `"Instructions"` | Panel switcher label                                        |
| `itemLabel`            | `string`                      | `"instruction"`  | Singular noun for count                                     |
| `headerActions`        | `ReactNode`                   | —                | Extra actions in the header toolbar (e.g. configure button) |
| `footer`               | `ReactNode`                   | —                | Extra footer content                                        |
| `footerAlign`          | `"left" \| "right"`           | `"right"`        | Where footer content is positioned                          |
| `showSelectionSummary` | `boolean`                     | `true`           | Show "X items selected" text                                |

### AttachmentPicker.Panel [#attachmentpickerpanel]

| Prop                   | Type                | Default   | Description                        |
| ---------------------- | ------------------- | --------- | ---------------------------------- |
| `id`                   | `string`            | —         | Unique panel identifier            |
| `label`                | `string`            | —         | Panel switcher label               |
| `children`             | `ReactNode`         | —         | Panel body content                 |
| `itemLabel`            | `string`            | `"item"`  | Singular noun for count            |
| `headerToolbar`        | `ReactNode`         | —         | Header toolbar content             |
| `footer`               | `ReactNode`         | —         | Extra footer content               |
| `footerAlign`          | `"left" \| "right"` | `"right"` | Where footer content is positioned |
| `showSelectionSummary` | `boolean`           | `true`    | Show "X items selected" text       |

### AttachmentPicker.usePanelSelection(panelId) [#attachmentpickerusepanelselectionpanelid]

Returns `{ selectedIds, setSelectedIds, toggleId, count }` for use in custom panels.

## Related [#related]

* [Files](/docs/composites/files/files) — Composable file management compound component
* [Toolbar](/docs/composites/files/toolbar) — Composable toolbar with search, sort, view toggle, and upload
* [FileListView](/docs/composites/files/list-view) — File list with selection support
