



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

This example keeps Cubitt responsible only for file visuals and selection
semantics. The app owns the `ContextMenu` components, action labels,
permissions, file/folder branching, destructive states, and persistence.

`useFilesContextMenu` and `useFilesContext` must be called inside `Files`. The
context menu hook resolves the menu target from the current selection and
returns prop getters for the menu root, the collection trigger, and each
`Files.Item`. The public Files context lets consumer-owned menu actions reuse
Cubitt interactions such as inline rename.

The same action renderer can be used from a right-click menu and from a toolbar
overflow menu. Applicability is handled by rendering different menu items for
single files, single folders, multiple files, and mixed selections.

```tsx title="app/components/files-context-menu-example.tsx"
import { useState } from "react";
import {
  Files,
  type FilesItem,
  type FilesViewMode,
} from "@tilt-legal/cubitt-components/files";
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 { Grid, Menu as MenuIcon } from "@tilt-legal/cubitt-icons/ui/outline";
import { FilesContextMenuSurface } from "@/components/files-context-menu-surface";
import { FilesSelectionActionsMenu } from "@/components/files-selection-actions-menu";
import { type ExampleFileSource, contextMenuItems } from "@/lib/files";

export function FilesContextMenuExample() {
  const [items, setItems] = useState(contextMenuItems);
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const [view, setView] = useState<FilesViewMode>("grid");

  return (
    <div className="flex flex-col gap-4">
      <Files
        onRename={(item: FilesItem<ExampleFileSource>, name) => {
          setItems((currentItems) =>
            currentItems.map((currentItem) =>
              currentItem.id === item.id
                ? { ...currentItem, name }
                : currentItem
            )
          );
          toast.success(`Renamed ${name}`);
        }}
        onSelectedIdsChange={setSelectedIds}
        selectedIds={selectedIds}
        selectionMode="multiple"
      >
        <Toolbar>
          <Toolbar.Heading>
            <Toolbar.Title>Files</Toolbar.Title>
            <Toolbar.Count>{items.length}</Toolbar.Count>
          </Toolbar.Heading>
          <Toolbar.Actions>
            <FilesSelectionActionsMenu
              items={items}
              onAction={(message) => toast.success(message)}
            />
            <ToggleGroup
              aria-label="View"
              groupVariant="segmented"
              multiple={false}
              onValueChange={(nextValue) => {
                if (nextValue === "grid" || nextValue === "list") {
                  setView(nextValue);
                }
              }}
              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>
          </Toolbar.Actions>
        </Toolbar>
        <FilesContextMenuSurface
          items={items}
          onAction={(message) => toast.success(message)}
          view={view}
        />
      </Files>
    </div>
  );
}
```

```tsx title="app/components/files-action-menu-items.tsx"
import type { ReactNode } from "react";
import {
  type FilesItem,
  useFilesContext,
} from "@tilt-legal/cubitt-components/files";
import {
  ContextMenuItem,
  ContextMenuSeparator,
} from "@tilt-legal/cubitt-components/context-menu";
import {
  MenuItem,
  MenuSeparator,
} from "@tilt-legal/cubitt-components/menu";
import {
  Archive,
  Download,
  FolderOpen,
  Pencil,
} from "@tilt-legal/cubitt-icons/ui/outline";
import type { ExampleFileSource } from "@/lib/files";

type ActionMenuKind = "context" | "toolbar";

function getPluralTargetLabel(
  items: FilesItem<ExampleFileSource>[],
  noun: "file" | "folder" | "item"
): string {
  return `${items.length} ${noun}${items.length === 1 ? "" : "s"}`;
}

function ActionMenuItem({
  children,
  disabled,
  kind,
  onClick,
  variant,
}: {
  children: ReactNode;
  disabled?: boolean;
  kind: ActionMenuKind;
  onClick?: () => void;
  variant?: "destructive";
}) {
  if (kind === "context") {
    return (
      <ContextMenuItem disabled={disabled} onClick={onClick} variant={variant}>
        {children}
      </ContextMenuItem>
    );
  }

  return (
    <MenuItem disabled={disabled} onClick={onClick} variant={variant}>
      {children}
    </MenuItem>
  );
}

function ActionMenuSeparator({ kind }: { kind: ActionMenuKind }) {
  return kind === "context" ? <ContextMenuSeparator /> : <MenuSeparator />;
}

export function FilesActionMenuItems({
  kind,
  onAction,
  targetItems,
}: {
  kind: ActionMenuKind;
  onAction: (message: string) => void;
  targetItems: FilesItem<ExampleFileSource>[];
}) {
  const filesContext = useFilesContext();

  if (targetItems.length === 0) {
    return (
      <ActionMenuItem disabled kind={kind}>
        No file selected
      </ActionMenuItem>
    );
  }

  const singleTarget = targetItems.length === 1 ? targetItems[0] : null;
  const fileTargets = targetItems.filter((item) => item.kind === "file");
  const folderTargets = targetItems.filter((item) => item.kind === "folder");
  const isAllFiles = fileTargets.length === targetItems.length;
  const isAllFolders = folderTargets.length === targetItems.length;

  if (singleTarget) {
    const isFolder = singleTarget.kind === "folder";

    return (
      <>
        <ActionMenuItem
          kind={kind}
          onClick={() => onAction(`Opened ${singleTarget.name}`)}
        >
          <FolderOpen />
          {isFolder ? "Open folder" : "Open"}
        </ActionMenuItem>
        {isFolder ? null : (
          <ActionMenuItem
            kind={kind}
            onClick={() => onAction(`Downloaded ${singleTarget.name}`)}
          >
            <Download />
            Download
          </ActionMenuItem>
        )}
        <ActionMenuItem
          kind={kind}
          onClick={() => filesContext.startRename(singleTarget)}
        >
          <Pencil />
          Rename
        </ActionMenuItem>
        <ActionMenuSeparator kind={kind} />
        <ActionMenuItem
          kind={kind}
          onClick={() => onAction(`Archived ${singleTarget.name}`)}
          variant="destructive"
        >
          <Archive />
          {isFolder ? "Archive folder" : "Archive file"}
        </ActionMenuItem>
      </>
    );
  }

  if (isAllFiles) {
    const fileLabel = getPluralTargetLabel(targetItems, "file");

    return (
      <>
        <ActionMenuItem
          kind={kind}
          onClick={() => onAction(`Downloaded ${fileLabel}`)}
        >
          <Download />
          Download {fileLabel}
        </ActionMenuItem>
        <ActionMenuSeparator kind={kind} />
        <ActionMenuItem
          kind={kind}
          onClick={() => onAction(`Archived ${fileLabel}`)}
          variant="destructive"
        >
          <Archive />
          Archive {fileLabel}
        </ActionMenuItem>
      </>
    );
  }

  const targetLabel = getPluralTargetLabel(
    targetItems,
    isAllFolders ? "folder" : "item"
  );

  return (
    <ActionMenuItem
      kind={kind}
      onClick={() => onAction(`Archived ${targetLabel}`)}
      variant="destructive"
    >
      <Archive />
      Archive {targetLabel}
    </ActionMenuItem>
  );
}
```

```tsx title="app/components/files-selection-actions-menu.tsx"
import {
  type FilesItem,
  useFilesContext,
} from "@tilt-legal/cubitt-components/files";
import { Button } from "@tilt-legal/cubitt-components/button";
import {
  Menu,
  MenuContent,
  MenuTrigger,
} from "@tilt-legal/cubitt-components/menu";
import { Dots } from "@tilt-legal/cubitt-icons/ui/outline";
import { FilesActionMenuItems } from "@/components/files-action-menu-items";
import type { ExampleFileSource } from "@/lib/files";

function getSelectionItems(
  items: FilesItem<ExampleFileSource>[],
  selectedIds: string[]
): FilesItem<ExampleFileSource>[] {
  const itemsById = new Map(items.map((item) => [item.id, item]));

  return selectedIds
    .map((id) => itemsById.get(id))
    .filter((item): item is FilesItem<ExampleFileSource> => Boolean(item));
}

export function FilesSelectionActionsMenu({
  items,
  onAction,
}: {
  items: FilesItem<ExampleFileSource>[];
  onAction: (message: string) => void;
}) {
  const filesContext = useFilesContext();
  const targetItems = getSelectionItems(items, filesContext.selectedIds);

  return (
    <Menu>
      <Button
        aria-label="File actions"
        mode="icon"
        render={<MenuTrigger />}
        variant="secondary"
      >
        <Dots />
      </Button>
      <MenuContent align="end">
        <FilesActionMenuItems
          kind="toolbar"
          onAction={onAction}
          targetItems={targetItems}
        />
      </MenuContent>
    </Menu>
  );
}
```

```tsx title="app/components/files-context-menu-surface.tsx"
import {
  Files,
  type FilesItem,
  type FilesViewMode,
  useFilesContextMenu,
} from "@tilt-legal/cubitt-components/files";
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuTrigger,
} from "@tilt-legal/cubitt-components/context-menu";
import { FilesActionMenuItems } from "@/components/files-action-menu-items";
import type { ExampleFileSource } from "@/lib/files";

type Props = {
  items: FilesItem<ExampleFileSource>[];
  onAction: (message: string) => void;
  view: FilesViewMode;
};

export function FilesContextMenuSurface({ items, onAction, view }: Props) {
  const contextMenu = useFilesContextMenu(items);

  return (
    <ContextMenu {...contextMenu.getRootProps()}>
      <ContextMenuTrigger
        render={(triggerProps) =>
          view === "grid" ? (
            <Files.Grid {...contextMenu.getTriggerProps(triggerProps)}>
              {items.map((item) => (
                <Files.Item
                  item={item}
                  key={item.id}
                  {...contextMenu.getItemProps(item)}
                />
              ))}
            </Files.Grid>
          ) : (
            <Files.List {...contextMenu.getTriggerProps(triggerProps)}>
              {items.map((item) => (
                <Files.Item
                  item={item}
                  key={item.id}
                  {...contextMenu.getItemProps(item)}
                />
              ))}
            </Files.List>
          )
        }
      />
      <ContextMenuContent>
        <FilesActionMenuItems
          kind="context"
          onAction={onAction}
          targetItems={contextMenu.targetItems}
        />
      </ContextMenuContent>
    </ContextMenu>
  );
}
```

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

export type ExampleFileSource = {
  owner: string;
};

export const contextMenuItems: FilesItem<ExampleFileSource>[] = [
  {
    id: "folder-discovery",
    kind: "folder",
    name: "Discovery",
    description: "24 items",
    source: { owner: "Sarah Chen" },
  },
  {
    id: "folder-depositions",
    kind: "folder",
    name: "Depositions",
    description: "8 items",
    source: { owner: "Ava Johnson" },
  },
  {
    id: "financial-report",
    kind: "file",
    name: "Q4 Financial Report.pdf",
    mediaType: "application/pdf",
    sizeLabel: "2.4 MB",
    description: "Sarah Chen",
    source: { owner: "Sarah Chen" },
  },
  {
    id: "board-minutes",
    kind: "file",
    name: "Board Minutes.docx",
    mediaType:
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    sizeLabel: "848 KB",
    description: "Ava Johnson",
    source: { owner: "Ava Johnson" },
  },
];
```

Right-clicking an unselected item targets only that item and updates selection.
Right-clicking an already selected item keeps the selected group as the target
set. Use `targetItems` for mixed file/folder rules and `targetIds` when calling
app-owned commands.
