



<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, useState } from "react";
import type {
  FilesItem,
  FilesUploadEntry,
  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,
} 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 {
  AttachmentFileSource,
  Instruction,
  Matter,
} from "@/lib/attachment-picker-data";

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

type Props = {
  files: FilesItem<AttachmentFileSource>[];
  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";

  return (
    <Dialog
      open={open}
      onOpenChange={(nextOpen) => {
        if (nextOpen) setOpen(true);
        else closePicker();
      }}
    >
      <DialogTrigger render={trigger} />
      <DialogContent
        className="w-full sm:h-137 sm:max-w-4xl"
        showDismissButton={!confirming}
      >
        <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>
        </DialogHeader>
        <DialogBody className="pt-.5 [&_[data-slot=table-header]]:hidden [&_tbody[aria-hidden]]:hidden">
          {activePanel === "files" ? (
            <AttachmentPickerFilesPanel
              disabled={confirming}
              items={files}
              onSelectedIdsChange={(ids) => setPanelSelection("files", ids)}
              onUpload={onUpload}
              query={filesQuery}
              selectedIds={selections.files}
              selectedMatters={selectedMatters}
              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">
          <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>
      </DialogContent>
    </Dialog>
  );
}
```

```tsx title="components/attachment-picker/files-panel.tsx"
import { useMemo } from "react";
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 { getFileLabel } from "@tilt-legal/cubitt-components/utilities/formatters";
import {
  matters,
  type AttachmentFileSource,
  type Matter,
} from "@/lib/attachment-picker-data";

type PanelProps = {
  disabled?: boolean;
  items: FilesItem<AttachmentFileSource>[];
  onUpload?: (entries: FilesUploadEntry[]) => void;
  query: string;
  selectedIds: string[];
  selectedMatters: Matter[];
  onSelectedIdsChange: (ids: string[]) => void;
  view: FilesViewMode;
};

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

function getFileType(item: FilesItem<AttachmentFileSource>) {
  return item.kind === "file"
    ? getFileLabel(item.name, item.mediaType)
    : "Folder";
}

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,
      mediaType: file.type,
      name: file.name,
      relativePath,
      size: file.size,
    };
  });
}

function FilesListHeader() {
  return (
    <div
      aria-hidden="true"
      className="grid h-10 grid-cols-[2rem_minmax(12rem,1fr)_8rem_7rem_8rem] items-center rounded-lg border border-border-3 bg-surface-1 font-medium text-fg-2 text-xs"
    >
      <span />
      <span className="px-1">Name</span>
      <span className="px-3">Type</span>
      <span className="px-3">Size</span>
      <span className="px-3">Updated</span>
    </div>
  );
}

function renderListFiles(items: FilesItem<AttachmentFileSource>[]) {
  return items.map((item) => (
    <Files.Item
      className="grid-cols-[2rem_minmax(12rem,1fr)_8rem_7rem_8rem]"
      item={item}
      key={item.id}
    >
      <div className="flex justify-center">
        <Files.ItemIcon />
      </div>
      <div className="flex min-w-0 items-center gap-1.5 py-2 ps-1 pe-3">
        <Files.ItemName />
      </div>
      <span className="truncate px-3 py-2 text-fg-2">{getFileType(item)}</span>
      <span className="truncate px-3 py-2 text-fg-2">{item.sizeLabel}</span>
      <span className="truncate px-3 py-2 text-fg-2">
        {item.source?.updatedLabel}
      </span>
    </Files.Item>
  ));
}

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,
  query,
  selectedIds,
  selectedMatters,
  onSelectedIdsChange,
  view,
}: PanelProps) {
  const visibleItems = useMemo(() => {
    const selectedMatterIds = new Set(selectedMatters.map((matter) => matter.id));
    const normalizedQuery = query.trim().toLowerCase();

    return items.filter((item) => {
      const matchesMatter =
        selectedMatterIds.size === 0 ||
        item.source?.matters.some((matter) => selectedMatterIds.has(matter.id));
      const matchesQuery =
        !normalizedQuery ||
        [item.name, item.description, item.sizeLabel, item.source?.owner]
          .filter((value): value is string => Boolean(value))
          .some((value) => value.toLowerCase().includes(normalizedQuery));

      return matchesMatter && matchesQuery;
    });
  }, [items, query, selectedMatters]);
  const isNoFilesState = items.length === 0;
  const isNoResultsState = items.length > 0 && visibleItems.length === 0;
  const isUploadEmptyState = Boolean(onUpload) && isNoFilesState;

  return (
    <Files
      className="min-h-0 flex-1"
      disabled={disabled}
      selectedIds={selectedIds}
      onSelectedIdsChange={onSelectedIdsChange}
      selectionBehavior="toggle"
      selectionMode="multiple"
    >
      <Files.Content className="flex min-h-0 flex-1 flex-col overflow-auto">
        {visibleItems.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>
            {visibleItems.map((item) => (
              <Files.Item item={item} key={item.id} />
            ))}
          </Files.Grid>
        ) : (
          <Files.List header={<FilesListHeader />}>
            {renderListFiles(visibleItems)}
          </Files.List>
        )}
      </Files.Content>
    </Files>
  );
}

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={`flex items-start gap-3 rounded-lg border border-border-4 bg-surface-1 px-3 py-2.5 text-left transition-colors hover:bg-surface-2 ${
              isSelected ? "border-brand-1 bg-brand-soft" : ""
            }`}
            key={instruction.id}
            onClick={() => toggleInstruction(instruction.id)}
            type="button"
          >
            <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";
import { formatBytes } from "@tilt-legal/cubitt-components/utilities/formatters";

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

export type AttachmentFileSource = {
  matters: Matter[];
  owner: string;
  size: number;
  updatedLabel: string;
};

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: FilesItem<AttachmentFileSource>[] = [
  {
    id: "financial-report",
    kind: "file",
    name: "Q4 Financial Report.pdf",
    mediaType: "application/pdf",
    sizeLabel: formatBytes(2_458_624),
    description: "Sarah Chen",
    source: {
      owner: "Sarah Chen",
      size: 2_458_624,
      updatedLabel: "Jan 6",
      matters: [matters[0]!],
    },
  },
  {
    id: "client-contract",
    kind: "file",
    name: "Client Contract.docx",
    mediaType:
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    sizeLabel: formatBytes(612_000),
    description: "Ava Johnson",
    source: {
      owner: "Ava Johnson",
      size: 612_000,
      updatedLabel: "Jan 6",
      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.
