Attachment Picker
A composable dialog for selecting files, instructions, or custom content types.
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.
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
AttachmentPicker supports two primary confirmation paths:
- Pick-only (sync) —
onConfirmreturnsvoidand the dialog closes based onautoClose. - Create/submit (async) —
onConfirmreturns aPromise; the picker locks interaction while pending. On resolve, it closes whenautoCloseis enabled. On reject, it stays open.
Golden Path
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.
<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):
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
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
mattersproperty - Upload — built-in upload button with MIME filtering and size validation (enabled when
onUploadClickis passed). PassuploadProgressto show real-time upload status, progress, and ETA per file - Selection — single or multiple file selection via
selectionMode
<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>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).
<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>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.
<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
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
The files panel supports 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"withmime_typeandsize - Virtual row:
asset_type: "virtual"withvirtual_type
When processing onConfirm selections, use asset_type / virtual_type on the source row to branch behavior when needed:
<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
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.
<AttachmentPicker.FilesPanel
files={files}
footer={<ProviderSelector value={provider} onChange={setProvider} />}
footerAlign="left"
showSelectionSummary={false}
/>Examples
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.
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>
);
}API Reference
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
| 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
| 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
| 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)
Returns { selectedIds, setSelectedIds, toggleId, count } for use in custom panels.
Related
- Files — Composable file management compound component
- Toolbar — Composable toolbar with search, sort, view toggle, and upload
- FileListView — File list with selection support