



<Preview className="min-h-[520px]">
  <FilesAttachmentPickerConsumerExample />
</Preview>

This example mirrors the original attachment picker flow: a two-panel dialog
for files and instructions, panel switching in the title, panel-specific
toolbars, matter filtering, upload affordances, active-panel selection summary,
and confirm locking. The preview includes state buttons for the docs surface;
the implementation below shows the consumer picker without those state shims.

```tsx title="components/attachment-picker/index.tsx"
import { type ReactElement, useMemo, useState } from "react";
import {
  Files,
  type FilesUploadEntry,
  type FilesViewMode,
} from "@tilt-legal/cubitt-components/files";
import { Button } from "@tilt-legal/cubitt-components/button";
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@tilt-legal/cubitt-components/dialog";
import { Toolbar } from "@tilt-legal/cubitt-components/toolbar";
import { formatPlural } from "@tilt-legal/cubitt-components/utilities/formatters";
import {
  AttachmentPickerFilesPanel,
  AttachmentPickerFilesToolbar,
  getVisibleAttachmentPickerFiles,
} from "@/components/attachment-picker-files-panel";
import {
  AttachmentPickerInstructionsPanel,
  AttachmentPickerInstructionsToolbar,
} from "@/components/attachment-picker-instructions-panel";
import { AttachmentPickerPanelSwitcher } from "@/components/attachment-picker-panel-switcher";
import type {
  AttachmentFileItem,
  Instruction,
  Matter,
} from "@/lib/attachment-picker-data";

type PanelId = "files" | "instructions";
type Selections = Record<PanelId, string[]>;

type Props = {
  files: AttachmentFileItem[];
  instructions: Instruction[];
  onConfirm: (selections: Selections) => void | Promise<void>;
  onUpload?: (entries: FilesUploadEntry[]) => void;
  trigger?: ReactElement;
};

const emptySelections = (): Selections => ({ files: [], instructions: [] });

export function MatterAttachmentPicker({
  files,
  instructions,
  onConfirm,
  onUpload,
  trigger = <Button variant="secondary">Attach Files</Button>,
}: Props) {
  const [open, setOpen] = useState(false);
  const [activePanel, setActivePanel] = useState<PanelId>("files");
  const [filesQuery, setFilesQuery] = useState("");
  const [instructionsQuery, setInstructionsQuery] = useState("");
  const [selectedMatters, setSelectedMatters] = useState<Matter[]>([]);
  const [selections, setSelections] = useState<Selections>(emptySelections);
  const [view, setView] = useState<FilesViewMode>("list");
  const [confirming, setConfirming] = useState(false);

  function closePicker() {
    setOpen(false);
    setActivePanel("files");
    setFilesQuery("");
    setInstructionsQuery("");
    setSelectedMatters([]);
    setSelections(emptySelections());
    setView("list");
    setConfirming(false);
  }

  function setPanelSelection(panelId: PanelId, selectedIds: string[]) {
    setSelections((current) => ({ ...current, [panelId]: selectedIds }));
  }

  async function handleConfirm() {
    const selectedIds = selections[activePanel];
    if (selectedIds.length === 0 || confirming) return;

    try {
      setConfirming(true);
      await onConfirm(selections);
      closePicker();
    } finally {
      setConfirming(false);
    }
  }

  const selectedIds = selections[activePanel];
  const itemLabel = activePanel === "files" ? "file" : "instruction";
  const visibleFiles = useMemo(
    () =>
      getVisibleAttachmentPickerFiles({
        items: files,
        query: filesQuery,
        selectedMatters,
      }),
    [files, filesQuery, selectedMatters]
  );
  const showFilesListHeader =
    activePanel === "files" && view === "list" && visibleFiles.length > 0;

  return (
    <Dialog
      open={open}
      onOpenChange={(nextOpen) => {
        if (nextOpen) setOpen(true);
        else closePicker();
      }}
    >
      <DialogTrigger render={trigger} />
      <Files
        className="gap-0"
        disabled={confirming || activePanel !== "files"}
        items={visibleFiles}
        metadataProperties={["createdBy", "createdAt", "updatedAt"]}
        onSelectedIdsChange={(ids) => setPanelSelection("files", ids)}
        render={
          <DialogContent
            className="w-full sm:h-137 sm:max-w-4xl"
            showDismissButton={!confirming}
          />
        }
        selectedIds={selections.files}
        selectionBehavior="toggle"
        selectionMode="multiple"
      >
        <DialogHeader className="py-3.5">
          <Toolbar>
            <Toolbar.Heading>
              <Toolbar.Title className="text-lg leading-none">
                <DialogTitle className="text-inherit leading-none">
                  Attach
                </DialogTitle>
              </Toolbar.Title>
              <AttachmentPickerPanelSwitcher
                activePanel={activePanel}
                disabled={confirming}
                onPanelChange={setActivePanel}
              />
            </Toolbar.Heading>
            {activePanel === "files" ? (
              <AttachmentPickerFilesToolbar
                disabled={confirming}
                onQueryChange={setFilesQuery}
                onSelectedMattersChange={setSelectedMatters}
                onUpload={onUpload}
                onViewChange={setView}
                query={filesQuery}
                selectedMatters={selectedMatters}
                view={view}
              />
            ) : (
              <AttachmentPickerInstructionsToolbar
                disabled={confirming}
                onQueryChange={setInstructionsQuery}
                query={instructionsQuery}
              />
            )}
          </Toolbar>
          {showFilesListHeader ? <Files.List.Header /> : null}
        </DialogHeader>
        <DialogBody className="pt-.5">
          {activePanel === "files" ? (
            <AttachmentPickerFilesPanel
              disabled={confirming}
              items={visibleFiles}
              onUpload={onUpload}
              totalItemCount={files.length}
              view={view}
            />
          ) : (
            <AttachmentPickerInstructionsPanel
              instructions={instructions}
              onSelectedIdsChange={(ids) =>
                setPanelSelection("instructions", ids)
              }
              query={instructionsQuery}
              selectedIds={selections.instructions}
            />
          )}
        </DialogBody>
        <DialogFooter
          className="items-center pl-6 sm:justify-between"
          data-files-selection-preserve="true"
        >
          <p aria-live="polite" className="text-fg-2 text-sm">
            {selectedIds.length === 0
              ? `No ${itemLabel}s selected`
              : `${formatPlural(selectedIds.length, itemLabel)} selected`}
          </p>
          <div className="flex items-center gap-1">
            <DialogClose disabled={confirming}>Cancel</DialogClose>
            <Button
              disabled={selectedIds.length === 0 || confirming}
              onClick={handleConfirm}
              pending={confirming}
            >
              Attach
            </Button>
          </div>
        </DialogFooter>
      </Files>
    </Dialog>
  );
}
```

```tsx title="components/attachment-picker/files-panel.tsx"
import {
  BarsFilter,
  FolderOpen,
  Grid,
  Menu as MenuIcon,
} from "@tilt-legal/cubitt-icons/ui/outline";
import {
  Files,
  type FilesItem,
  type FilesUploadEntry,
  type FilesViewMode,
} from "@tilt-legal/cubitt-components/files";
import { Button } from "@tilt-legal/cubitt-components/button";
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxList,
  ComboboxSeparator,
  ComboboxTrigger,
} from "@tilt-legal/cubitt-components/combobox";
import {
  Dropzone,
  DropzoneFolderAnimation,
  type FileRejection,
} from "@tilt-legal/cubitt-components/dropzone";
import {
  EmptyState,
  EmptyStateDescription,
  EmptyStateHeading,
  EmptyStateMedia,
  EmptyStateTitle,
} from "@tilt-legal/cubitt-components/empty-state";
import { Input } from "@tilt-legal/cubitt-components/input";
import { SearchExpand } from "@tilt-legal/cubitt-components/search-expand";
import { toast } from "@tilt-legal/cubitt-components/toast";
import {
  ToggleGroup,
  ToggleGroupItem,
} from "@tilt-legal/cubitt-components/toggle-group";
import { Toolbar } from "@tilt-legal/cubitt-components/toolbar";
import {
  matters,
  type AttachmentFileItem,
  type Matter,
} from "@/lib/attachment-picker-data";

type PanelProps = {
  disabled?: boolean;
  items: FilesItem[];
  onUpload?: (entries: FilesUploadEntry[]) => void;
  totalItemCount: number;
  view: FilesViewMode;
};

export function getVisibleAttachmentPickerFiles({
  items,
  query,
  selectedMatters,
}: {
  items: AttachmentFileItem[];
  query: string;
  selectedMatters: Matter[];
}): AttachmentFileItem[] {
  const selectedMatterIds = new Set(selectedMatters.map((matter) => matter.id));

  return items.filter((item) => {
    const matchesMatter =
      selectedMatterIds.size === 0 ||
      item.matters.some((matter) => selectedMatterIds.has(matter.id));
    const matchesQuery =
      !query.trim() ||
      [
        item.name,
        typeof item.createdBy === "string" ? item.createdBy : undefined,
        formatBytes(item.size ?? 0),
      ]
        .filter((value): value is string => Boolean(value))
        .some((value) => value.toLowerCase().includes(query.toLowerCase()));

    return matchesMatter && matchesQuery;
  });
}

const ATTACHMENT_UPLOAD_ACCEPT = [
  "application/pdf",
  "image/*",
  "text/plain",
] as const;

function createUploadEntries(files: File[]): FilesUploadEntry[] {
  return files.map((file) => {
    const fileWithPath = file as File & {
      path?: string;
      webkitRelativePath?: string;
    };
    const relativePath =
      fileWithPath.webkitRelativePath || fileWithPath.path || undefined;

    return {
      id: relativePath || `${file.name}-${file.size}-${file.lastModified}`,
      file,
      mimeType: file.type,
      name: file.name,
      relativePath,
      size: file.size,
    };
  });
}

function renderListFiles(items: FilesItem[]) {
  return items.map((item) => <Files.Item item={item} key={item.id} />);
}

function UploadDropzone({
  disabled,
  onUpload,
}: {
  disabled?: boolean;
  onUpload?: (entries: FilesUploadEntry[]) => void;
}) {
  return (
    <Dropzone
      accept={ATTACHMENT_UPLOAD_ACCEPT}
      className="min-h-0 flex-1 self-stretch gap-1"
      isDisabled={disabled}
      onFileDrop={(files) => onUpload?.(createUploadEntries(files))}
      onFilesRejected={(rejections: FileRejection[]) => {
        for (const rejection of rejections) {
          toast.error(rejection.message);
        }
      }}
    >
      <DropzoneFolderAnimation showFileTypeIcons />
      <EmptyStateHeading>
        <EmptyStateTitle>Drop files here to upload</EmptyStateTitle>
        <EmptyStateDescription>
          or click to browse from your computer
        </EmptyStateDescription>
      </EmptyStateHeading>
    </Dropzone>
  );
}

export function AttachmentPickerFilesPanel({
  disabled,
  items,
  onUpload,
  totalItemCount,
  view,
}: PanelProps) {
  const isNoFilesState = totalItemCount === 0;
  const isNoResultsState = totalItemCount > 0 && items.length === 0;
  const isUploadEmptyState = Boolean(onUpload) && isNoFilesState;

  return (
    <Files.Content className="flex min-h-0 flex-1 flex-col overflow-auto">
      {items.length === 0 ? (
        isUploadEmptyState ? (
          <UploadDropzone disabled={disabled} onUpload={onUpload} />
        ) : (
          <div className="flex min-h-0 flex-1 items-center justify-center">
            <EmptyState variant="ghost">
              <EmptyStateMedia>
                <FolderOpen />
              </EmptyStateMedia>
              <EmptyStateHeading>
                <EmptyStateTitle>
                  {isNoResultsState ? "No results found" : "No files available"}
                </EmptyStateTitle>
                <EmptyStateDescription>
                  {isNoFilesState
                    ? "There are no files to select from."
                    : "Try a different search or matter filter."}
                </EmptyStateDescription>
              </EmptyStateHeading>
            </EmptyState>
          </div>
        )
      ) : view === "grid" ? (
        <Files.Grid>
          {items.map((item) => (
            <Files.Item item={item} key={item.id} />
          ))}
        </Files.Grid>
      ) : (
        <Files.List showHeader={false}>
          {renderListFiles(items)}
        </Files.List>
      )}
    </Files.Content>
  );
}

export function AttachmentPickerFilesToolbar({
  disabled,
  onQueryChange,
  onSelectedMattersChange,
  onUpload,
  onViewChange,
  query,
  selectedMatters,
  view,
}: {
  disabled?: boolean;
  query: string;
  selectedMatters: Matter[];
  view: FilesViewMode;
  onQueryChange: (value: string) => void;
  onSelectedMattersChange: (matters: Matter[]) => void;
  onUpload?: (entries: FilesUploadEntry[]) => void;
  onViewChange: (value: FilesViewMode) => void;
}) {
  return (
    <Toolbar.Actions data-files-selection-preserve="true">
      <SearchExpand
        collapseOnBlurWhenEmpty
        expandedWidth="14rem"
        label="Search files"
      >
        <Input
          aria-label="Search files"
          disabled={disabled}
          onChange={(event) => onQueryChange(event.currentTarget.value)}
          placeholder="Search files"
          value={query}
        />
      </SearchExpand>
      <Combobox
        items={matters}
        multiple
        onValueChange={(value) =>
          onSelectedMattersChange(Array.isArray(value) ? value : [])
        }
        value={selectedMatters}
      >
        <ComboboxTrigger
          render={<Button disabled={disabled} mode="icon" variant="secondary" />}
        >
          <BarsFilter />
        </ComboboxTrigger>
        <ComboboxContent align="end">
          <ComboboxInput placeholder="Search matters..." />
          <ComboboxSeparator />
          <ComboboxEmpty>No matters found.</ComboboxEmpty>
          <ComboboxList>{(matter: Matter) => (
            <ComboboxItem key={matter.id} value={matter}>
              <ComboboxItemIndicator />
              {matter.value}
            </ComboboxItem>
          )}</ComboboxList>
        </ComboboxContent>
      </Combobox>
      <Toolbar.Group>
        <ToggleGroup
          aria-label="View options"
          disabled={disabled}
          groupVariant="segmented"
          multiple={false}
          onValueChange={(value) => {
            if (value === "grid" || value === "list") onViewChange(value);
          }}
          value={view}
        >
          <ToggleGroupItem aria-label="Grid view" mode="icon" value="grid">
            <Grid />
          </ToggleGroupItem>
          <ToggleGroupItem aria-label="List view" mode="icon" value="list">
            <MenuIcon />
          </ToggleGroupItem>
        </ToggleGroup>
        {onUpload ? (
          <>
            <Toolbar.Separator />
            <Files.UploadButton
              accept={ATTACHMENT_UPLOAD_ACCEPT}
              disabled={disabled}
              onFilesAccepted={onUpload}
            />
          </>
        ) : null}
      </Toolbar.Group>
    </Toolbar.Actions>
  );
}
```

```tsx title="components/attachment-picker/instructions-panel.tsx"
import { useMemo } from "react";
import { Checkbox } from "@tilt-legal/cubitt-components/checkbox";
import { Input } from "@tilt-legal/cubitt-components/input";
import { SearchExpand } from "@tilt-legal/cubitt-components/search-expand";
import { Toolbar } from "@tilt-legal/cubitt-components/toolbar";
import type { Instruction } from "@/lib/attachment-picker-data";

type PanelProps = {
  instructions: Instruction[];
  query: string;
  selectedIds: string[];
  onSelectedIdsChange: (ids: string[]) => void;
};

export function AttachmentPickerInstructionsPanel({
  instructions,
  query,
  selectedIds,
  onSelectedIdsChange,
}: PanelProps) {
  const visibleInstructions = useMemo(() => {
    const normalizedQuery = query.trim().toLowerCase();
    if (!normalizedQuery) return instructions;

    return instructions.filter((instruction) =>
      [instruction.name, instruction.description].some((value) =>
        value.toLowerCase().includes(normalizedQuery)
      )
    );
  }, [instructions, query]);

  function toggleInstruction(id: string) {
    onSelectedIdsChange(
      selectedIds.includes(id)
        ? selectedIds.filter((selectedId) => selectedId !== id)
        : [...selectedIds, id]
    );
  }

  return (
    <div className="grid grid-cols-1 gap-1 sm:grid-cols-2">
      {visibleInstructions.map((instruction) => {
        const isSelected = selectedIds.includes(instruction.id);

        return (
          <Button
            className="h-auto justify-start gap-3 px-3 py-2.5 text-left"
            key={instruction.id}
            onClick={() => toggleInstruction(instruction.id)}
            type="button"
            variant="secondary"
          >
            <Checkbox checked={isSelected} className="mt-0.5 shrink-0" tabIndex={-1} />
            <div className="min-w-0">
              <p className="font-medium text-fg-1 text-sm">{instruction.name}</p>
              <p className="text-fg-2 text-xs">{instruction.description}</p>
            </div>
          </Button>
        );
      })}
      {visibleInstructions.length === 0 ? (
        <p className="col-span-full py-8 text-center text-fg-2 text-sm">
          No instructions found.
        </p>
      ) : null}
    </div>
  );
}

export function AttachmentPickerInstructionsToolbar({
  disabled,
  onQueryChange,
  query,
}: {
  disabled?: boolean;
  query: string;
  onQueryChange: (value: string) => void;
}) {
  return (
    <Toolbar.Actions data-files-selection-preserve="true">
      <SearchExpand
        collapseOnBlurWhenEmpty
        expandedWidth="14rem"
        label="Search instructions"
      >
        <Input
          aria-label="Search instructions"
          disabled={disabled}
          onChange={(event) => onQueryChange(event.currentTarget.value)}
          placeholder="Search instructions"
          value={query}
        />
      </SearchExpand>
    </Toolbar.Actions>
  );
}
```

```tsx title="components/attachment-picker/panel-switcher.tsx"
import {
  Menu,
  MenuContent,
  MenuRadioGroup,
  MenuRadioItem,
  MenuTrigger,
} from "@tilt-legal/cubitt-components/menu";
import { Toolbar } from "@tilt-legal/cubitt-components/toolbar";

type PanelId = "files" | "instructions";

export function AttachmentPickerPanelSwitcher({
  activePanel,
  disabled,
  onPanelChange,
}: {
  activePanel: PanelId;
  disabled?: boolean;
  onPanelChange: (panel: PanelId) => void;
}) {
  const activeLabel = activePanel === "files" ? "Files" : "Instructions";

  return (
    <Menu>
      <MenuTrigger
        render={
          <Toolbar.TitleTrigger
            className="text-lg leading-none"
            disabled={disabled}
          >
            {activeLabel}
          </Toolbar.TitleTrigger>
        }
      />
      <MenuContent align="start">
        <MenuRadioGroup
          onValueChange={(value) => {
            if (value === "files" || value === "instructions") {
              onPanelChange(value);
            }
          }}
          value={activePanel}
        >
          <MenuRadioItem closeOnClick value="files">
            Files
          </MenuRadioItem>
          <MenuRadioItem closeOnClick value="instructions">
            Instructions
          </MenuRadioItem>
        </MenuRadioGroup>
      </MenuContent>
    </Menu>
  );
}
```

```typescript title="components/attachment-picker/data.ts"
import type { FilesItem } from "@tilt-legal/cubitt-components/files";

export type Matter = { id: string; value: string };
export type Instruction = { id: string; name: string; description: string };

export type AttachmentFileItem = FilesItem & {
  matters: Matter[];
};

export const matters = [
  { id: "m1", value: "Smith v. Anderson" },
  { id: "m2", value: "Johnson Estate Planning" },
  { id: "m3", value: "Tech Corp Acquisition" },
] satisfies Matter[];

export const files: AttachmentFileItem[] = [
  {
    id: "financial-report",
    kind: "file",
    name: "Q4 Financial Report.pdf",
    mimeType: "application/pdf",
    size: 2_458_624,
    createdBy: "Sarah Chen",
    updatedAt: "2026-01-06",
    matters: [matters[0]!],
  },
  {
    id: "client-contract",
    kind: "file",
    name: "Client Contract.docx",
    mimeType:
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    size: 612_000,
    createdBy: "Ava Johnson",
    updatedAt: "2026-01-06",
    matters: [matters[0]!, matters[2]!],
  },
];

export const instructions: Instruction[] = [
  {
    id: "i1",
    name: "Professional Email",
    description: "Draft a professional email response with appropriate tone.",
  },
  {
    id: "i2",
    name: "Contract Summary",
    description: "Summarize key terms, obligations, and deadlines.",
  },
];
```

Use the docs preview buttons only to exercise states like empty uploads,
no files, and no results. In application code, pass those states through props
from your actual data and upload workflow.
