



<Preview className="min-h-[680px]">
  <FilesExplorerConsumerExample />
</Preview>

This example shows the consumer-owned shape for a full file manager. Cubitt
provides Files selection, inline rename, upload input wiring, details display,
and primitive menus. The app owns the directory model, navigation, persistence,
permissions, action labels, and the behavior behind each menu item.

```tsx title="apps/docs/src/components/examples/files-explorer/file-explorer-example.tsx"
import { useMemo, useState } from "react";
import {
  Files,
  type FilesItem,
  type FilesMoveEvent,
  type FilesUploadEntry,
  type FilesViewMode,
  useFilesContext,
} from "@tilt-legal/cubitt-components/files";
import { initialExplorerItems, ROOT_DIRECTORY_ID } from "./data";
import { FileExplorerDetailsPanel } from "./details-panel";
import { FileExplorerSurface } from "./file-surface";
import { FileExplorerToolbar } from "./toolbar";
import type {
  ExplorerActionHandlers,
  ExplorerItem,
  ExplorerSource,
} from "./types";
import {
  canMoveExplorerItems,
  createFolderItem,
  createUploadedItems,
  downloadFakeFile,
  getChildren,
  getDirectoryDetailsItem,
  getDirectoryPath,
  getMoveDestinationDirectoryId,
  getSelectedItems,
  moveExplorerItems,
  openFakeFile,
  removeItemsAndDescendants,
} from "./utils";

function FileExplorerInner(props: {
  currentDirectoryId: string;
  detailsOpen: boolean;
  items: ExplorerItem[];
  onCurrentDirectoryChange: (directoryId: string) => void;
  onDetailsOpenChange: (open: boolean) => void;
  onItemsChange: (items: ExplorerItem[]) => void;
  onSelectedIdsChange: (ids: string[]) => void;
  onViewChange: (view: FilesViewMode) => void;
  view: FilesViewMode;
}) {
  const filesContext = useFilesContext();
  const visibleItems = useMemo(
    () => getChildren(props.items, props.currentDirectoryId),
    [props.currentDirectoryId, props.items],
  );
  const selectedItems = useMemo(
    () => getSelectedItems(props.items, filesContext.selectedIds),
    [filesContext.selectedIds, props.items],
  );
  const currentPath = useMemo(
    () => getDirectoryPath(props.items, props.currentDirectoryId),
    [props.currentDirectoryId, props.items],
  );
  const detailsItems =
    selectedItems.length > 0
      ? selectedItems
      : [getDirectoryDetailsItem(props.items, props.currentDirectoryId)];

  function navigateToDirectory(directoryId: string) {
    props.onCurrentDirectoryChange(directoryId);
    props.onSelectedIdsChange([]);
  }

  function createFolder() {
    const folder = createFolderItem(props.currentDirectoryId);
    props.onItemsChange([...props.items, folder]);
    props.onSelectedIdsChange([folder.id]);
    filesContext.startRename(folder);
  }

  const actionHandlers: ExplorerActionHandlers = {
    onArchive: (items) => {
      props.onItemsChange(removeItemsAndDescendants(props.items, items));
      props.onSelectedIdsChange([]);
    },
    onCreateFolder: createFolder,
    onDownload: (items) => {
      items.filter((item) => item.kind === "file").forEach(downloadFakeFile);
    },
    onOpen: (item) => {
      if (item.kind === "folder") {
        navigateToDirectory(item.id);
        return;
      }

      openFakeFile(item);
    },
    onRename: filesContext.startRename,
    onViewDetails: () => props.onDetailsOpenChange(true),
  };

  return (
    <>
      <div className="sticky top-0 z-20 bg-background pb-4">
        <FileExplorerToolbar
          actionHandlers={actionHandlers}
          currentPath={currentPath}
          itemCount={visibleItems.length}
          items={props.items}
          onFilesAccepted={(entries: FilesUploadEntry[]) => {
            const uploadedItems = createUploadedItems(
              entries,
              props.currentDirectoryId,
            );
            props.onItemsChange([...props.items, ...uploadedItems]);
            props.onSelectedIdsChange(uploadedItems.map((item) => item.id));
          }}
          onNavigate={navigateToDirectory}
          onViewChange={props.onViewChange}
          view={props.view}
        />
      </div>
      <div className="flex min-h-[calc(100%-4rem)]">
        <Files.Content
          className="min-h-[30rem]"
          moveDestinationId={props.currentDirectoryId}
        >
          <FileExplorerSurface
            actionHandlers={actionHandlers}
            items={visibleItems}
            view={props.view}
          />
        </Files.Content>
        <FileExplorerDetailsPanel
          items={detailsItems}
          onClose={() => props.onDetailsOpenChange(false)}
          open={props.detailsOpen}
        />
      </div>
    </>
  );
}

export function FilesExplorerConsumerExample() {
  const [items, setItems] = useState<ExplorerItem[]>(initialExplorerItems);
  const [currentDirectoryId, setCurrentDirectoryId] =
    useState(ROOT_DIRECTORY_ID);
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const [view, setView] = useState<FilesViewMode>("grid");
  const [detailsOpen, setDetailsOpen] = useState(false);

  function renameItem(item: FilesItem<ExplorerSource>, name: string) {
    setItems((currentItems) =>
      currentItems.map((currentItem) =>
        currentItem.id === item.id ? { ...currentItem, name } : currentItem,
      ),
    );
  }

  const currentItem = useMemo(
    () => getDirectoryDetailsItem(items, currentDirectoryId),
    [currentDirectoryId, items],
  );

  function moveItems(event: FilesMoveEvent<ExplorerSource>) {
    const destinationId = getMoveDestinationDirectoryId(event);

    setItems((currentItems) =>
      moveExplorerItems(currentItems, event.items, destinationId),
    );
    setSelectedIds(event.items.map((item) => item.id));
  }

  return (
    <Files
      canMove={(event) => canMoveExplorerItems(items, event)}
      canRename={(item) => item.id !== ROOT_DIRECTORY_ID}
      className="h-[560px] w-full max-w-6xl overflow-y-auto"
      detailsItem={currentItem}
      items={items}
      onMove={moveItems}
      onRename={renameItem}
      onSelectedIdsChange={setSelectedIds}
      selectedIds={selectedIds}
      selectionBehavior="replace"
      selectionMode="multiple"
    >
      <FileExplorerInner
        currentDirectoryId={currentDirectoryId}
        detailsOpen={detailsOpen}
        items={items}
        onCurrentDirectoryChange={setCurrentDirectoryId}
        onDetailsOpenChange={setDetailsOpen}
        onItemsChange={setItems}
        onSelectedIdsChange={setSelectedIds}
        onViewChange={setView}
        view={view}
      />
    </Files>
  );
}
```

```tsx title="apps/docs/src/components/examples/files-explorer/toolbar.tsx"
import { Fragment } from "react";
import {
  BreadcrumbIcon,
  BreadcrumbItem,
  BreadcrumbLabel,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@tilt-legal/cubitt-components/breadcrumb";
import { Button } from "@tilt-legal/cubitt-components/button";
import {
  Menu,
  MenuContent,
  MenuTrigger,
} from "@tilt-legal/cubitt-components/menu";
import {
  ToggleGroup,
  ToggleGroupItem,
} from "@tilt-legal/cubitt-components/toggle-group";
import { Toolbar } from "@tilt-legal/cubitt-components/toolbar";
import {
  Dots,
  Grid,
  Menu as MenuIcon,
} from "@tilt-legal/cubitt-icons/ui/outline";
import {
  Files,
  type FilesUploadEntry,
  type FilesViewMode,
  useFilesContext,
} from "@tilt-legal/cubitt-components/files";
import { FileExplorerActionMenuItems } from "./file-actions";
import { getSelectedItems } from "./utils";
import type {
  ExplorerActionHandlers,
  ExplorerFolder,
  ExplorerItem,
} from "./types";

export function FileExplorerToolbar({
  actionHandlers,
  currentPath,
  itemCount,
  items,
  onFilesAccepted,
  onNavigate,
  onViewChange,
  view,
}: {
  actionHandlers: ExplorerActionHandlers;
  currentPath: ExplorerFolder[];
  itemCount: number;
  items: ExplorerItem[];
  onFilesAccepted: (entries: FilesUploadEntry[]) => void;
  onNavigate: (directoryId: string) => void;
  onViewChange: (view: FilesViewMode) => void;
  view: FilesViewMode;
}) {
  const filesContext = useFilesContext();
  const selectedItems = getSelectedItems(items, filesContext.selectedIds);

  return (
    <Toolbar size="md">
      <Toolbar.Heading className="min-w-0">
        {currentPath.length === 1 ? (
          <>
            <Toolbar.Title>Files</Toolbar.Title>
            <Toolbar.Count>{itemCount}</Toolbar.Count>
          </>
        ) : (
          <Toolbar.Breadcrumb className="@container w-full">
            <BreadcrumbList>
              <BreadcrumbItem className="shrink-0">
                <BreadcrumbLink
                  onClick={() => onNavigate(currentPath[0]?.id ?? "root")}
                  render={<button type="button" />}
                >
                  <BreadcrumbLabel className="shrink-0">Files</BreadcrumbLabel>
                </BreadcrumbLink>
              </BreadcrumbItem>
              {currentPath.slice(1).map((directory, index, path) => {
                const isCurrent = index === path.length - 1;

                return (
                  <Fragment key={directory.id}>
                    <BreadcrumbSeparator>/</BreadcrumbSeparator>
                    <BreadcrumbItem
                      className={isCurrent ? "min-w-0" : "shrink-0"}
                    >
                      {isCurrent ? (
                        <BreadcrumbPage className="min-w-0">
                          <BreadcrumbIcon>
                            <Files.FolderIcon />
                          </BreadcrumbIcon>
                          <BreadcrumbLabel>{directory.name}</BreadcrumbLabel>
                        </BreadcrumbPage>
                      ) : (
                        <BreadcrumbLink
                          onClick={() => onNavigate(directory.id)}
                          render={<button type="button" />}
                        >
                          <BreadcrumbIcon>
                            <Files.FolderIcon />
                          </BreadcrumbIcon>
                          <BreadcrumbLabel className="shrink-0">
                            {directory.name}
                          </BreadcrumbLabel>
                        </BreadcrumbLink>
                      )}
                    </BreadcrumbItem>
                  </Fragment>
                );
              })}
            </BreadcrumbList>
          </Toolbar.Breadcrumb>
        )}
      </Toolbar.Heading>
      <Toolbar.Actions>
        <Menu>
          <Button
            aria-label="File actions"
            mode="icon"
            render={<MenuTrigger />}
            variant="secondary"
          >
            <Dots />
          </Button>
          <MenuContent align="end">
            <FileExplorerActionMenuItems
              handlers={actionHandlers}
              kind="toolbar"
              showCreateFolder
              targetItems={selectedItems}
            />
          </MenuContent>
        </Menu>
        <ToggleGroup
          aria-label="View"
          groupVariant="segmented"
          multiple={false}
          onValueChange={(nextValue) => {
            if (nextValue === "grid" || nextValue === "list") {
              onViewChange(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.Separator />
        <Files.UploadButton multiple onFilesAccepted={onFilesAccepted} />
      </Toolbar.Actions>
    </Toolbar>
  );
}
```

```tsx title="apps/docs/src/components/examples/files-explorer/file-surface.tsx"
import {
  Files,
  type FilesViewMode,
  useFilesContextMenu,
} from "@tilt-legal/cubitt-components/files";
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuTrigger,
} from "@tilt-legal/cubitt-components/context-menu";
import { FileExplorerActionMenuItems } from "./file-actions";
import type { ExplorerActionHandlers, ExplorerItem } from "./types";

export function FileExplorerSurface({
  actionHandlers,
  items,
  view,
}: {
  actionHandlers: ExplorerActionHandlers;
  items: ExplorerItem[];
  view: FilesViewMode;
}) {
  const contextMenu = useFilesContextMenu(items);

  return (
    <ContextMenu {...contextMenu.getRootProps()}>
      <ContextMenuTrigger
        render={(triggerProps) =>
          view === "grid" ? (
            <Files.Grid
              className="min-h-full"
              {...contextMenu.getTriggerProps(triggerProps)}
            >
              {items.map((item) => (
                <Files.Item
                  item={item}
                  key={item.id}
                  onOpen={actionHandlers.onOpen}
                  {...contextMenu.getItemProps(item)}
                />
              ))}
            </Files.Grid>
          ) : (
            <Files.List
              className="min-h-full"
              {...contextMenu.getTriggerProps(triggerProps)}
            >
              {items.map((item) => (
                <Files.Item
                  item={item}
                  key={item.id}
                  onOpen={actionHandlers.onOpen}
                  {...contextMenu.getItemProps(item)}
                />
              ))}
            </Files.List>
          )
        }
      />
      <ContextMenuContent>
        <FileExplorerActionMenuItems
          handlers={actionHandlers}
          kind="context"
          showCreateFolder={contextMenu.targetItems.length === 0}
          targetItems={contextMenu.targetItems}
        />
      </ContextMenuContent>
    </ContextMenu>
  );
}
```

```tsx title="apps/docs/src/components/examples/files-explorer/file-actions.tsx"
import type { ReactNode } from "react";
import {
  CircleInfo,
  Download4,
  FolderPlus,
  Pen2,
  Trash,
} from "@tilt-legal/cubitt-icons/ui/outline";
import {
  ContextMenuItem,
  ContextMenuSeparator,
} from "@tilt-legal/cubitt-components/context-menu";
import {
  MenuItem,
  MenuSeparator,
} from "@tilt-legal/cubitt-components/menu";
import { formatCount } from "./utils";
import type {
  ExplorerActionHandlers,
  ExplorerActionMenuKind,
  ExplorerItem,
} from "./types";

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

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

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

function CreateFolderMenuItem({
  handlers,
  kind,
}: {
  handlers: ExplorerActionHandlers;
  kind: ExplorerActionMenuKind;
}) {
  return (
    <ActionMenuItem kind={kind} onClick={handlers.onCreateFolder}>
      <FolderPlus />
      New folder
    </ActionMenuItem>
  );
}

function ViewDetailsMenuItem({
  handlers,
  kind,
}: {
  handlers: ExplorerActionHandlers;
  kind: ExplorerActionMenuKind;
}) {
  return (
    <ActionMenuItem kind={kind} onClick={handlers.onViewDetails}>
      <CircleInfo />
      View details
    </ActionMenuItem>
  );
}

export function FileExplorerActionMenuItems({
  handlers,
  kind,
  showCreateFolder = false,
  targetItems,
}: {
  handlers: ExplorerActionHandlers;
  kind: ExplorerActionMenuKind;
  showCreateFolder?: boolean;
  targetItems: ExplorerItem[];
}) {
  if (targetItems.length === 0) {
    return (
      <>
        <CreateFolderMenuItem handlers={handlers} kind={kind} />
        <ViewDetailsMenuItem handlers={handlers} kind={kind} />
      </>
    );
  }

  return (
    <>
      {showCreateFolder ? (
        <>
          <CreateFolderMenuItem handlers={handlers} kind={kind} />
          <ViewDetailsMenuItem handlers={handlers} kind={kind} />
          <ActionMenuSeparator kind={kind} />
        </>
      ) : null}
      <TargetActionMenuItems
        handlers={handlers}
        kind={kind}
        showViewDetails={!showCreateFolder}
        targetItems={targetItems}
      />
    </>
  );
}
```

```tsx title="apps/docs/src/components/examples/files-explorer/details-panel.tsx"
import { Files } from "@tilt-legal/cubitt-components/files";
import { Button } from "@tilt-legal/cubitt-components/button";
import { Separator } from "@tilt-legal/cubitt-components/separator";
import { formatBytes } from "@tilt-legal/cubitt-components/utilities/formatters";
import { Xmark } from "@tilt-legal/cubitt-icons/ui/outline";
import type { ExplorerItem } from "./types";
import { formatCount } from "./utils";

export function FileExplorerDetailsPanel({
  items,
  onClose,
  open,
}: {
  items: ExplorerItem[];
  onClose: () => void;
  open: boolean;
}) {
  const singleItem = items.length === 1 ? items[0] : null;
  const ownerCount = new Set(
    items
      .map((item) => item.source?.owner)
      .filter((owner): owner is string => Boolean(owner)),
  ).size;
  const statusCount = new Set(
    items
      .map((item) => item.source?.status)
      .filter(
        (status): status is NonNullable<ExplorerItem["source"]>["status"] =>
          Boolean(status),
      ),
  ).size;
  const totalSizeLabel = items.every((item) => typeof item.size === "number")
    ? formatBytes(
        items.reduce((totalSize, item) => totalSize + (item.size ?? 0), 0),
      )
    : null;

  return (
    <div
      aria-hidden={!open}
      className={
        open
          ? "sticky top-16 h-[calc(560px-4rem)] w-[400px] shrink-0 overflow-hidden border-l border-border-3 opacity-100"
          : "w-0 shrink-0 overflow-hidden opacity-0"
      }
    >
      <Button
        aria-label="Close details"
        className="absolute top-1 right-1 z-10"
        mode="icon"
        onClick={onClose}
        type="button"
        variant="secondary"
      >
        <Xmark />
      </Button>
      <Files.Details className="h-full min-w-0 ps-6">
        <Files.Details.Icon />
        <Files.Details.Header>
          <Files.Details.Heading>
            <Files.Details.Name />
            <Files.Details.Type />
          </Files.Details.Heading>
          <Files.Details.EditNameButton />
        </Files.Details.Header>
        <Files.Details.Panels>
          <Files.Details.Panel label="Details" value="details">
            <Separator />
            <Files.Details.Properties>
              {singleItem?.source?.owner ? (
                <Files.Details.Property>
                  <Files.Details.Property.Label>
                    Owner
                  </Files.Details.Property.Label>
                  <Files.Details.Property.Value>
                    {singleItem.source.owner}
                  </Files.Details.Property.Value>
                </Files.Details.Property>
              ) : null}
              {singleItem?.source?.status ? (
                <Files.Details.Property>
                  <Files.Details.Property.Label>
                    Status
                  </Files.Details.Property.Label>
                  <Files.Details.Property.Value>
                    {singleItem.source.status}
                  </Files.Details.Property.Value>
                </Files.Details.Property>
              ) : null}
              {singleItem?.source?.updatedLabel ? (
                <Files.Details.Property>
                  <Files.Details.Property.Label>
                    Updated
                  </Files.Details.Property.Label>
                  <Files.Details.Property.Value>
                    {singleItem.source.updatedLabel}
                  </Files.Details.Property.Value>
                </Files.Details.Property>
              ) : null}
              {singleItem?.sizeLabel ? (
                <Files.Details.Property>
                  <Files.Details.Property.Label>
                    Size
                  </Files.Details.Property.Label>
                  <Files.Details.Property.Value>
                    {singleItem.sizeLabel}
                  </Files.Details.Property.Value>
                </Files.Details.Property>
              ) : null}
              {items.length > 1 ? (
                <Files.Details.Property>
                  <Files.Details.Property.Label>
                    Files selected
                  </Files.Details.Property.Label>
                  <Files.Details.Property.Value>
                    {items.length}
                  </Files.Details.Property.Value>
                </Files.Details.Property>
              ) : null}
              {items.length > 1 && totalSizeLabel ? (
                <Files.Details.Property>
                  <Files.Details.Property.Label>
                    Total size
                  </Files.Details.Property.Label>
                  <Files.Details.Property.Value>
                    {totalSizeLabel}
                  </Files.Details.Property.Value>
                </Files.Details.Property>
              ) : null}
              {items.length > 1 ? (
                <Files.Details.Property>
                  <Files.Details.Property.Label>
                    Owners
                  </Files.Details.Property.Label>
                  <Files.Details.Property.Value>
                    {formatCount(ownerCount, "owner")}
                  </Files.Details.Property.Value>
                </Files.Details.Property>
              ) : null}
              {items.length > 1 ? (
                <Files.Details.Property>
                  <Files.Details.Property.Label>
                    Statuses
                  </Files.Details.Property.Label>
                  <Files.Details.Property.Value>
                    {formatCount(statusCount, "status")}
                  </Files.Details.Property.Value>
                </Files.Details.Property>
              ) : null}
            </Files.Details.Properties>
          </Files.Details.Panel>
        </Files.Details.Panels>
      </Files.Details>
    </div>
  );
}
```

```typescript title="apps/docs/src/components/examples/files-explorer/data.ts"
import type { ExplorerFolder, ExplorerItem } from "./types";

export const ROOT_DIRECTORY_ID = "root";

export const rootDirectory: ExplorerFolder = {
  id: ROOT_DIRECTORY_ID,
  kind: "folder",
  name: "Files",
  description: "Workspace root",
  source: {
    owner: "Matter team",
    parentId: null,
    status: "Reviewed",
    updatedLabel: "Today",
  },
};

export const initialExplorerItems: ExplorerItem[] = [
  {
    id: "folder-discovery",
    kind: "folder",
    name: "Discovery",
    description: "4 items",
    source: {
      owner: "Sarah Chen",
      parentId: ROOT_DIRECTORY_ID,
      status: "Draft",
      updatedLabel: "Today",
    },
  },
  {
    id: "folder-depositions",
    kind: "folder",
    name: "Depositions",
    description: "3 items",
    source: {
      owner: "Ava Johnson",
      parentId: ROOT_DIRECTORY_ID,
      status: "Reviewed",
      updatedLabel: "Yesterday",
    },
  },
  {
    id: "financial-report",
    kind: "file",
    name: "Q4 Financial Report.pdf",
    mediaType: "application/pdf",
    size: 2_458_624,
    sizeLabel: "2.4 MB",
    description: "Sarah Chen",
    source: {
      owner: "Sarah Chen",
      parentId: ROOT_DIRECTORY_ID,
      status: "Reviewed",
      updatedLabel: "2 days ago",
    },
  },
];
```

```typescript title="apps/docs/src/components/examples/files-explorer/utils.ts"
import type {
  FilesMoveEvent,
  FilesUploadEntry,
} from "@tilt-legal/cubitt-components/files";
import { formatBytes } from "@tilt-legal/cubitt-components/utilities/formatters";
import { ROOT_DIRECTORY_ID, rootDirectory } from "./data";
import type { ExplorerFolder, ExplorerItem, ExplorerSource } from "./types";

export function getChildren(
  items: ExplorerItem[],
  directoryId: string,
): ExplorerItem[] {
  return items
    .filter((item) => item.source?.parentId === directoryId)
    .sort((first, second) => {
      if (first.kind !== second.kind) {
        return first.kind === "folder" ? -1 : 1;
      }

      return first.name.localeCompare(second.name);
    });
}

export function getSelectedItems(
  items: ExplorerItem[],
  selectedIds: string[],
): ExplorerItem[] {
  const itemsById = new Map(items.map((item) => [item.id, item]));

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

export function getDirectoryPath(
  items: ExplorerItem[],
  directoryId: string,
): ExplorerFolder[] {
  const foldersById = new Map(
    items
      .filter((item): item is ExplorerFolder => item.kind === "folder")
      .map((folder) => [folder.id, folder]),
  );
  const path: ExplorerFolder[] = [];
  let currentDirectory =
    directoryId === ROOT_DIRECTORY_ID
      ? rootDirectory
      : (foldersById.get(directoryId) ?? rootDirectory);

  while (currentDirectory.id !== ROOT_DIRECTORY_ID) {
    path.unshift(currentDirectory);
    const parentId = currentDirectory.source?.parentId ?? ROOT_DIRECTORY_ID;
    currentDirectory =
      parentId === ROOT_DIRECTORY_ID
        ? rootDirectory
        : (foldersById.get(parentId) ?? rootDirectory);
  }

  return [rootDirectory, ...path];
}

export function createFolderItem(directoryId: string): ExplorerFolder {
  return {
    id: `folder-${globalThis.crypto.randomUUID()}`,
    kind: "folder",
    name: "Untitled folder",
    description: "0 items",
    source: {
      owner: "You",
      parentId: directoryId,
      status: "Draft",
      updatedLabel: "Just now",
    },
  };
}

export function createUploadedItems(
  entries: FilesUploadEntry[],
  directoryId: string,
): ExplorerItem[] {
  return entries.map((entry) => ({
    id: `file-${globalThis.crypto.randomUUID()}`,
    kind: "file",
    name: entry.name,
    mediaType: entry.mediaType,
    size: entry.size,
    sizeLabel: formatBytes(entry.size),
    description: "You",
    source: {
      owner: "You",
      parentId: directoryId,
      status: "Draft",
      updatedLabel: "Just now",
    },
  }));
}

export function removeItemsAndDescendants(
  items: ExplorerItem[],
  removedItems: ExplorerItem[],
): ExplorerItem[] {
  const removedIds = new Set(removedItems.map((item) => item.id));
  let changed = true;

  while (changed) {
    changed = false;

    for (const item of items) {
      if (
        !removedIds.has(item.id) &&
        item.source?.parentId &&
        removedIds.has(item.source.parentId)
      ) {
        removedIds.add(item.id);
        changed = true;
      }
    }
  }

  return items.filter((item) => !removedIds.has(item.id));
}

export function canMoveExplorerItems(
  items: ExplorerItem[],
  event: FilesMoveEvent<ExplorerSource>,
): boolean {
  const destinationId = getMoveDestinationDirectoryId(event);
  const movingIds = new Set(event.items.map((item) => item.id));

  if (event.destination.type === "folder" && movingIds.has(destinationId)) {
    return false;
  }

  if (event.items.every((item) => item.source?.parentId === destinationId)) {
    return false;
  }

  return event.items.every((item) => {
    if (item.kind !== "folder") {
      return true;
    }

    return !isDirectoryDescendant(items, item.id, destinationId);
  });
}

export function moveExplorerItems(
  items: ExplorerItem[],
  movedItems: ExplorerItem[],
  destinationId: string,
): ExplorerItem[] {
  const movedIds = new Set(movedItems.map((item) => item.id));

  return items.map((item) =>
    movedIds.has(item.id)
      ? {
          ...item,
          source: {
            owner: item.source?.owner ?? "You",
            parentId: destinationId,
            status: item.source?.status ?? "Draft",
            updatedLabel: "Just now",
          },
        }
      : item,
  );
}

export function getMoveDestinationDirectoryId(
  event: FilesMoveEvent<ExplorerSource>,
): string {
  return event.destination.type === "folder"
    ? event.destination.item.id
    : (event.destination.id ?? ROOT_DIRECTORY_ID);
}

function isDirectoryDescendant(
  items: ExplorerItem[],
  ancestorId: string,
  directoryId: string,
): boolean {
  const foldersById = new Map(
    items
      .filter((item): item is ExplorerFolder => item.kind === "folder")
      .map((folder) => [folder.id, folder]),
  );
  let currentDirectoryId: string | null = directoryId;

  while (currentDirectoryId && currentDirectoryId !== ROOT_DIRECTORY_ID) {
    if (currentDirectoryId === ancestorId) {
      return true;
    }

    currentDirectoryId =
      foldersById.get(currentDirectoryId)?.source?.parentId ??
      ROOT_DIRECTORY_ID;
  }

  return false;
}
```

```typescript title="apps/docs/src/components/examples/files-explorer/types.ts"
import type { FilesItem } from "@tilt-legal/cubitt-components/files";

export type ExplorerSource = {
  owner: string;
  parentId: string | null;
  status: "Draft" | "Reviewed" | "Filed";
  updatedLabel: string;
};

export type ExplorerItem = FilesItem<ExplorerSource>;
export type ExplorerFolder = Extract<ExplorerItem, { kind: "folder" }>;
export type ExplorerActionMenuKind = "context" | "toolbar";

export type ExplorerActionHandlers = {
  onArchive: (items: ExplorerItem[]) => void;
  onCreateFolder: () => void;
  onDownload: (items: ExplorerItem[]) => void;
  onOpen: (item: ExplorerItem) => void;
  onRename: (item: ExplorerItem) => void;
  onViewDetails: () => void;
};
```
