{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-system",
  "title": "File System (Finder)",
  "description": "A macOS Finder-style file browser for flat object-store manifests with icon, list, column, and gallery views.",
  "dependencies": [
    "@pierre/trees@1.0.0-beta.4",
    "@base-ui/react@^1.4.1"
  ],
  "registryDependencies": [
    "@extend/docx-viewer",
    "@extend/file-thumbnail",
    "@extend/pdf-viewer",
    "@extend/pptx-viewer",
    "utils",
    "@extend/xlsx-viewer",
    "button",
    "command",
    "dialog",
    "dropdown-menu",
    "input",
    "popover",
    "scroll-area",
    "select",
    "tabs"
  ],
  "files": [
    {
      "path": "components/extend/file-system.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\"\nimport {\n  createFileTreeIconResolver,\n  getBuiltInSpriteSheet,\n  prepareFileTreeInput,\n  type FileTreeSortComparator,\n  type FileTreeSortEntry,\n} from \"@pierre/trees\"\nimport { FileTree as PierreFileTree, useFileTree } from \"@pierre/trees/react\"\nimport { createPortal } from \"react-dom\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\"\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport {\n  ScrollArea as InlineScrollArea,\n  ScrollBar,\n} from \"@/components/ui/scroll-area\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { FileThumbnail } from \"@/components/extend/file-thumbnail\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\nfunction ArrowDown01Glyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ChevronDown\"\n      tabler=\"IconChevronDown\"\n      hugeicons=\"ArrowDown01Icon\"\n      phosphor=\"CaretDownIcon\"\n      remixicon=\"RiArrowDownSLine\"\n      {...props}\n    />\n  )\n}\nfunction ArrowUp01Glyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"ChevronUp\"\n      tabler=\"IconChevronUp\"\n      hugeicons=\"ArrowUp01Icon\"\n      phosphor=\"CaretUpIcon\"\n      remixicon=\"RiArrowUpSLine\"\n      {...props}\n    />\n  )\n}\nfunction Calendar03Glyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Calendar\"\n      tabler=\"IconCalendar\"\n      hugeicons=\"Calendar03Icon\"\n      phosphor=\"CalendarIcon\"\n      remixicon=\"RiCalendarLine\"\n      {...props}\n    />\n  )\n}\nfunction File01Glyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"FileArchiveIcon\"\n      tabler=\"IconFileZip\"\n      hugeicons=\"File01Icon\"\n      phosphor=\"FileZipIcon\"\n      remixicon=\"RiFileZipLine\"\n      {...props}\n    />\n  )\n}\nfunction GalleryThumbnailsGlyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"GalleryThumbnails\"\n      tabler=\"IconLayoutBottombar\"\n      hugeicons=\"GalleryThumbnailsIcon\"\n      phosphor=\"ImagesIcon\"\n      remixicon=\"RiGalleryView\"\n      {...props}\n    />\n  )\n}\nfunction GridViewGlyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"LayoutGrid\"\n      tabler=\"IconLayoutGrid\"\n      hugeicons=\"GridViewIcon\"\n      phosphor=\"SquaresFourIcon\"\n      remixicon=\"RiLayoutGridLine\"\n      {...props}\n    />\n  )\n}\nfunction LayoutThreeColumnGlyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Columns3\"\n      tabler=\"IconColumns3\"\n      hugeicons=\"LayoutThreeColumnIcon\"\n      phosphor=\"ColumnsIcon\"\n      remixicon=\"RiLayoutColumnLine\"\n      {...props}\n    />\n  )\n}\nfunction LeftToRightListBulletGlyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"Columns3Icon\"\n      tabler=\"IconLayoutColumns\"\n      hugeicons=\"LeftToRightListBulletIcon\"\n      phosphor=\"ColumnsIcon\"\n      remixicon=\"RiLayoutColumnLine\"\n      {...props}\n    />\n  )\n}\nconst LazyPDFViewer = React.lazy(() =>\n  import(\"@/components/extend/pdf-viewer\").then((mod) => ({\n    default: mod.PDFViewer,\n  }))\n)\nconst LazyDocxViewerPreview = React.lazy(() =>\n  import(\"@/components/extend/docx-viewer\").then((mod) => ({\n    default: mod.DocxViewerPreview,\n  }))\n)\nconst LazyPptxViewerPreview = React.lazy(() =>\n  import(\"@/components/extend/pptx-viewer\").then((mod) => ({\n    default: mod.PptxViewerPreview,\n  }))\n)\nconst LazyXlsxViewerPreview = React.lazy(() =>\n  import(\"@/components/extend/xlsx-viewer\").then((mod) => ({\n    default: mod.XlsxViewerPreview,\n  }))\n)\nexport type FileSystemView = \"icons\" | \"list\" | \"columns\" | \"gallery\"\nexport type FileSystemFolderItem = {\n  kind: \"folder\"\n  /** Folder prefix, e.g. `\"invoices/2026/\"`. A trailing slash is added when missing. */\n  path: string\n  name?: string\n  parentPath?: string\n  /** Set when children exist but are not in `items` yet; enables `loadChildren`. */\n  hasChildren?: boolean\n  createdAt?: string\n  updatedAt?: string\n}\nexport type FileSystemFileItem = {\n  kind: \"file\"\n  /** Display/canonical path, e.g. `\"invoices/2026/jan.pdf\"`. */\n  path: string\n  /** Original object key (S3/R2). Defaults to `path`. */\n  key?: string\n  name?: string\n  parentPath?: string\n  contentType?: string\n  size?: number\n  createdAt?: string\n  updatedAt?: string\n  etag?: string\n  /** Optional if already public/presigned. Otherwise resolved via `getFileUrl`. */\n  url?: string\n  /** Externally generated thumbnail. The component never renders documents itself. */\n  previewImageUrl?: string | null\n  /**\n   * Externally generated page thumbnails (first entry is the cover). When a\n   * file has more than one page, large thumbnails show a hover pager.\n   */\n  previewImageUrls?: string[] | null\n  /**\n   * Total page count when it exceeds `previewImageUrls.length`; the pager\n   * loads the remaining pages on demand via `loadPreviewImageUrl`.\n   */\n  previewPageCount?: number\n  /** Thumbnail aspect ratio (width / height). Defaults to a portrait page. */\n  previewAspectRatio?: number\n  metadata?: Record<string, string>\n}\nexport type FileSystemItem = FileSystemFolderItem | FileSystemFileItem\nexport type FileSystemLoadChildrenArgs = {\n  path: string\n  cursor: string | null\n}\nexport type FileSystemLoadChildrenResult = {\n  items: FileSystemItem[]\n  nextCursor?: string | null\n}\nexport type FileSystemProps = {\n  /** Flat manifest. Folders are optional; missing prefixes are inferred from file paths. */\n  items: FileSystemItem[]\n  className?: string\n  /** Label for the root folder. */\n  title?: string\n  defaultView?: FileSystemView\n  view?: FileSystemView\n  onViewChange?: (view: FileSystemView) => void\n  /** Folder prefix to open initially, e.g. `\"invoices/\"`. */\n  defaultPath?: string\n  onSelectionChange?: (item: FileSystemItem | null) => void\n  /**\n   * Called on file open (double-click), replacing the built-in behavior. By\n   * default PDF, DOCX, PPTX, XLSX, and image files open in a viewer dialog and\n   * other files open their resolved URL in a new tab.\n   */\n  onFileOpen?: (file: FileSystemFileItem, url: string | null) => void\n  /** Resolve a URL (e.g. presigned) for a file without one. */\n  getFileUrl?: (file: FileSystemFileItem) => string | Promise<string>\n  /** Lazily fetch children for folders with `hasChildren` and no loaded entries. */\n  loadChildren?: (\n    args: FileSystemLoadChildrenArgs\n  ) => Promise<FileSystemLoadChildrenResult>\n  /** Custom preview node for files without `previewImageUrl`. */\n  renderFilePreview?: (file: FileSystemFileItem) => React.ReactNode\n  /**\n   * Lazily render a page thumbnail beyond the eagerly provided\n   * `previewImageUrls` (the pager calls this as pages come into view).\n   */\n  loadPreviewImageUrl?: (\n    file: FileSystemFileItem,\n    pageIndex: number\n  ) => Promise<string | null>\n}\ntype FolderEntry = FileSystemFolderItem & {\n  name: string\n  parentPath: string\n}\ntype FileEntry = FileSystemFileItem & {\n  key: string\n  name: string\n  parentPath: string\n}\ntype FileSystemEntry = FolderEntry | FileEntry\ntype FileSystemIndex = {\n  children: Map<string, FileSystemEntry[]>\n  files: Map<string, FileEntry>\n  folders: Map<string, FolderEntry>\n}\nfunction normalizeFolderPath(path: string) {\n  if (!path || path === \"/\") return \"\"\n  return path.endsWith(\"/\") ? path : `${path}/`\n}\nfunction pathName(path: string) {\n  const trimmed = path.endsWith(\"/\") ? path.slice(0, -1) : path\n  const separatorIndex = trimmed.lastIndexOf(\"/\")\n  return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 1)\n}\nfunction pathParent(path: string) {\n  const trimmed = path.endsWith(\"/\") ? path.slice(0, -1) : path\n  const separatorIndex = trimmed.lastIndexOf(\"/\")\n  return separatorIndex === -1 ? \"\" : trimmed.slice(0, separatorIndex + 1)\n}\nfunction fileExtension(name: string) {\n  const dotIndex = name.lastIndexOf(\".\")\n  return dotIndex === -1 ? \"\" : name.slice(dotIndex + 1).toLowerCase()\n}\nconst FILE_KIND_LABELS: Record<string, string> = {\n  css: \"CSS Stylesheet\",\n  csv: \"CSV Document\",\n  doc: \"Word Document\",\n  docx: \"Word Document\",\n  gif: \"GIF Image\",\n  go: \"Go Source\",\n  jpeg: \"JPEG Image\",\n  jpg: \"JPEG Image\",\n  js: \"JavaScript Source\",\n  json: \"JSON Document\",\n  jsx: \"JavaScript Source\",\n  md: \"Markdown Document\",\n  mdx: \"MDX Document\",\n  pdf: \"PDF Document\",\n  png: \"PNG Image\",\n  ppt: \"PowerPoint Presentation\",\n  pptx: \"PowerPoint Presentation\",\n  py: \"Python Script\",\n  rs: \"Rust Source\",\n  sh: \"Shell Script\",\n  sql: \"SQL Script\",\n  svg: \"SVG Image\",\n  ts: \"TypeScript Source\",\n  tsv: \"TSV Document\",\n  tsx: \"TypeScript Source\",\n  txt: \"Plain Text\",\n  webp: \"WebP Image\",\n  xls: \"Excel Workbook\",\n  xlsx: \"Excel Workbook\",\n  yaml: \"YAML Document\",\n  yml: \"YAML Document\",\n  zip: \"ZIP Archive\",\n}\nfunction fileKindLabel(file: FileEntry) {\n  const byExtension = FILE_KIND_LABELS[fileExtension(file.name)]\n  if (byExtension) return byExtension\n  if (file.contentType?.startsWith(\"image/\")) return \"Image\"\n  return file.contentType ?? \"Document\"\n}\n// Folders sort under the \"Folder\" kind alphabetically among the file kinds,\n// like Finder's Kind sort.\nfunction entryKindLabel(entry: FileSystemEntry) {\n  return entry.kind === \"folder\" ? \"Folder\" : fileKindLabel(entry)\n}\n// MIME types inferred from the extension when a file carries no\n// `contentType`, so the file-type filter can classify every manifest entry.\nconst EXTENSION_MIME_TYPES: Record<string, string> = {\n  css: \"text/css\",\n  csv: \"text/csv\",\n  doc: \"application/msword\",\n  docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n  gif: \"image/gif\",\n  go: \"text/x-go\",\n  jpeg: \"image/jpeg\",\n  jpg: \"image/jpeg\",\n  js: \"text/javascript\",\n  json: \"application/json\",\n  jsx: \"text/jsx\",\n  md: \"text/markdown\",\n  mdx: \"text/mdx\",\n  pdf: \"application/pdf\",\n  png: \"image/png\",\n  ppt: \"application/vnd.ms-powerpoint\",\n  pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n  py: \"text/x-python\",\n  rs: \"text/x-rust\",\n  sh: \"application/x-sh\",\n  sql: \"application/sql\",\n  svg: \"image/svg+xml\",\n  ts: \"text/x-typescript\",\n  tsv: \"text/tab-separated-values\",\n  tsx: \"text/x-typescript\",\n  txt: \"text/plain\",\n  webp: \"image/webp\",\n  xls: \"application/vnd.ms-excel\",\n  xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n  yaml: \"text/yaml\",\n  yml: \"text/yaml\",\n  zip: \"application/zip\",\n}\nconst FALLBACK_MIME_TYPE = \"application/octet-stream\"\nconst IPAD_MIN_WIDTH = 768\nconst MIME_TYPE_LABELS: Record<string, string> = {\n  [FALLBACK_MIME_TYPE]: \"Binary\",\n  \"application/json\": \"JSON\",\n  \"application/msword\": \"Word document (legacy)\",\n  \"application/pdf\": \"PDF\",\n  \"application/sql\": \"SQL\",\n  \"application/vnd.ms-excel\": \"Excel workbook (legacy)\",\n  \"application/vnd.ms-powerpoint\": \"PowerPoint (legacy)\",\n  \"application/vnd.openxmlformats-officedocument.presentationml.presentation\":\n    \"PowerPoint\",\n  \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\":\n    \"Excel workbook\",\n  \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\":\n    \"Word document\",\n  \"application/x-sh\": \"Shell script\",\n  \"application/zip\": \"ZIP archive\",\n  \"image/gif\": \"GIF image\",\n  \"image/jpeg\": \"JPEG image\",\n  \"image/png\": \"PNG image\",\n  \"image/svg+xml\": \"SVG image\",\n  \"image/webp\": \"WebP image\",\n  \"text/css\": \"CSS\",\n  \"text/csv\": \"CSV\",\n  \"text/javascript\": \"JavaScript\",\n  \"text/jsx\": \"JSX\",\n  \"text/markdown\": \"Markdown\",\n  \"text/mdx\": \"MDX\",\n  \"text/plain\": \"Plain text\",\n  \"text/tab-separated-values\": \"TSV\",\n  \"text/x-go\": \"Go\",\n  \"text/x-python\": \"Python\",\n  \"text/x-rust\": \"Rust\",\n  \"text/x-typescript\": \"TypeScript\",\n  \"text/yaml\": \"YAML\",\n}\nfunction mimeTypeForFile(file: FileEntry) {\n  return (\n    file.contentType ??\n    EXTENSION_MIME_TYPES[fileExtension(file.name)] ??\n    FALLBACK_MIME_TYPE\n  )\n}\nfunction fileTypeFilterGroup(mime: string): FileTypeFilterGroup {\n  if (\n    mime === \"application/pdf\" ||\n    mime === \"application/msword\" ||\n    mime === \"application/vnd.ms-powerpoint\" ||\n    mime ===\n      \"application/vnd.openxmlformats-officedocument.presentationml.presentation\" ||\n    mime ===\n      \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n  ) {\n    return \"Documents\"\n  }\n  if (\n    mime === \"application/vnd.ms-excel\" ||\n    mime ===\n      \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\" ||\n    mime === \"text/csv\" ||\n    mime === \"text/tab-separated-values\"\n  ) {\n    return \"Spreadsheets\"\n  }\n  if (mime.startsWith(\"image/\")) return \"Images\"\n  if (\n    mime === \"application/json\" ||\n    mime === \"application/sql\" ||\n    mime === \"application/x-sh\" ||\n    mime === \"text/css\" ||\n    mime === \"text/javascript\" ||\n    mime === \"text/jsx\" ||\n    mime === \"text/x-go\" ||\n    mime === \"text/x-python\" ||\n    mime === \"text/x-rust\" ||\n    mime === \"text/x-typescript\" ||\n    mime === \"text/yaml\"\n  ) {\n    return \"Code\"\n  }\n  if (\n    mime === \"text/markdown\" ||\n    mime === \"text/mdx\" ||\n    mime === \"text/plain\"\n  ) {\n    return \"Text\"\n  }\n  return \"Archives & binary\"\n}\nexport type FileSystemViewerKind = \"docx\" | \"image\" | \"pdf\" | \"pptx\" | \"xlsx\"\nfunction viewerKindForFile(\n  file: FileSystemFileItem\n): FileSystemViewerKind | null {\n  if (file.contentType?.startsWith(\"image/\")) return \"image\"\n  if (file.contentType === \"application/pdf\") return \"pdf\"\n  if (\n    file.contentType ===\n    \"application/vnd.openxmlformats-officedocument.presentationml.presentation\"\n  ) {\n    return \"pptx\"\n  }\n  const name = (file.name ?? file.path).toLowerCase()\n  if (name.endsWith(\".pdf\")) return \"pdf\"\n  if (name.endsWith(\".docx\")) return \"docx\"\n  if (name.endsWith(\".pptx\")) return \"pptx\"\n  if (name.endsWith(\".xlsx\")) return \"xlsx\"\n  if (/\\.(avif|gif|jpe?g|png|svg|webp)$/.test(name)) return \"image\"\n  return null\n}\n// PDF and Word pages want height; presentations and spreadsheets want width;\n// images get a roomy but contained frame.\nconst VIEWER_DIALOG_CLASSNAMES: Record<FileSystemViewerKind, string> = {\n  docx: \"h-[88vh] w-[min(96vw,68rem)] max-w-none\",\n  image: \"max-h-[88vh] w-fit max-w-[min(96vw,64rem)]\",\n  pdf: \"h-[88vh] w-[min(96vw,68rem)] max-w-none\",\n  pptx: \"h-[88vh] w-[min(96vw,84rem)] max-w-none\",\n  xlsx: \"h-[85vh] w-[min(96vw,100rem)] max-w-none\",\n}\nfunction FileSystemViewerLoading() {\n  return (\n    <div className=\"grid h-full min-h-48 flex-1 place-items-center bg-background\">\n      <InlineSpinner className=\"size-4 text-muted-foreground\" />\n    </div>\n  )\n}\nfunction formatByteSize(size: number | undefined) {\n  if (size === undefined) return null\n  if (size < 1000) return `${size} bytes`\n  const units = [\"KB\", \"MB\", \"GB\", \"TB\"]\n  let value = size\n  for (const unit of units) {\n    value /= 1000\n    if (value < 1000 || unit === \"TB\") {\n      return `${value >= 100 ? Math.round(value) : value.toFixed(value >= 10 ? 1 : 2).replace(/\\.?0+$/, \"\")} ${unit}`\n    }\n  }\n  return null\n}\nfunction formatTimestamp(value: string | undefined) {\n  if (!value) return null\n  const date = new Date(value)\n  if (Number.isNaN(date.getTime())) return null\n  const day = date.toLocaleDateString(\"en-US\", {\n    day: \"numeric\",\n    month: \"short\",\n    year: \"numeric\",\n  })\n  const time = date.toLocaleTimeString(\"en-US\", {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n  })\n  return `${day} at ${time}`\n}\n// Every directory prefix appearing in the given relative file paths.\nfunction directoryPathsOf(paths: readonly string[]) {\n  const directoryPaths = new Set<string>()\n  for (const relativePath of paths) {\n    let slashIndex = relativePath.indexOf(\"/\")\n    while (slashIndex !== -1) {\n      directoryPaths.add(relativePath.slice(0, slashIndex))\n      slashIndex = relativePath.indexOf(\"/\", slashIndex + 1)\n    }\n  }\n  return directoryPaths\n}\nfunction compareEntryNames(\n  left: {\n    name: string\n  },\n  right: {\n    name: string\n  }\n) {\n  return left.name.localeCompare(right.name, undefined, {\n    numeric: true,\n    sensitivity: \"base\",\n  })\n}\nexport type FileSystemSortKey =\n  | \"createdAt\"\n  | \"kind\"\n  | \"name\"\n  | \"size\"\n  | \"updatedAt\"\ntype FileSystemSortState = {\n  direction: \"asc\" | \"desc\"\n  key: FileSystemSortKey\n}\nconst SORT_OPTIONS: Array<{\n  defaultDirection: \"asc\" | \"desc\"\n  key: FileSystemSortKey\n  label: string\n  /** Shorter label so the toolbar trigger stays narrow. */\n  triggerLabel: string\n}> = [\n  { defaultDirection: \"asc\", key: \"name\", label: \"Name\", triggerLabel: \"Name\" },\n  { defaultDirection: \"asc\", key: \"kind\", label: \"Kind\", triggerLabel: \"Kind\" },\n  {\n    defaultDirection: \"desc\",\n    key: \"createdAt\",\n    label: \"Date created\",\n    triggerLabel: \"Created\",\n  },\n  {\n    defaultDirection: \"desc\",\n    key: \"updatedAt\",\n    label: \"Date modified\",\n    triggerLabel: \"Modified\",\n  },\n  {\n    defaultDirection: \"desc\",\n    key: \"size\",\n    label: \"Size\",\n    triggerLabel: \"Size\",\n  },\n]\nconst DEFAULT_SORT: FileSystemSortState = { direction: \"asc\", key: \"name\" }\nfunction defaultSortDirection(key: FileSystemSortKey) {\n  return (\n    SORT_OPTIONS.find((option) => option.key === key)?.defaultDirection ?? \"asc\"\n  )\n}\nfunction entrySortTimestamp(\n  entry: FileSystemEntry,\n  key: \"createdAt\" | \"updatedAt\"\n) {\n  const value = entry[key]\n  const time = value ? Date.parse(value) : Number.NaN\n  return Number.isNaN(time) ? 0 : time\n}\n// Primary key per the active sort; ties (and missing metadata) fall back to\n// the name order so results stay stable. The name tiebreak ignores the\n// direction, like Finder.\nfunction compareEntriesBySort(\n  left: FileSystemEntry,\n  right: FileSystemEntry,\n  sort: FileSystemSortState\n) {\n  let result = 0\n  if (sort.key === \"name\") {\n    result = compareEntryNames(left, right)\n  } else if (sort.key === \"kind\") {\n    result = entryKindLabel(left).localeCompare(\n      entryKindLabel(right),\n      undefined,\n      {\n        sensitivity: \"base\",\n      }\n    )\n  } else if (sort.key === \"size\") {\n    // Folders have no byte size; group them at the small end.\n    const leftSize = left.kind === \"file\" ? (left.size ?? 0) : -1\n    const rightSize = right.kind === \"file\" ? (right.size ?? 0) : -1\n    result = leftSize - rightSize\n  } else {\n    result =\n      entrySortTimestamp(left, sort.key) - entrySortTimestamp(right, sort.key)\n  }\n  if (result === 0) return compareEntryNames(left, right)\n  return sort.direction === \"asc\" ? (result < 0 ? -1 : 1) : result < 0 ? 1 : -1\n}\nexport type FileSystemFilterType = \"dateCreated\" | \"dateModified\" | \"fileType\"\ntype FileSystemDateFilterType = Exclude<FileSystemFilterType, \"fileType\">\ntype FileSystemFilterOperator =\n  | \"after\"\n  | \"before\"\n  | \"in-range\"\n  | \"is\"\n  | \"is-any-of\"\n  | \"is-not\"\n  | \"not-in-range\"\ntype FileSystemFilter = {\n  id: string\n  operator: FileSystemFilterOperator\n  type: FileSystemFilterType\n  value: string[]\n}\ntype FileTypeFilterGroup =\n  | \"Documents\"\n  | \"Spreadsheets\"\n  | \"Images\"\n  | \"Code\"\n  | \"Text\"\n  | \"Archives & binary\"\ntype FileTypeFilterOption = {\n  group: FileTypeFilterGroup\n  /** Sample file name so the option icon reuses the file-type sprite. */\n  iconFileName: string\n  label: string\n  mime: string\n}\nconst FILE_TYPE_FILTER_GROUPS: FileTypeFilterGroup[] = [\n  \"Documents\",\n  \"Spreadsheets\",\n  \"Images\",\n  \"Code\",\n  \"Text\",\n  \"Archives & binary\",\n]\nconst FILTER_TYPE_LABELS: Record<FileSystemFilterType, string> = {\n  dateCreated: \"Date created\",\n  dateModified: \"Date modified\",\n  fileType: \"File type\",\n}\nconst FILTER_OPERATOR_LABELS: Record<FileSystemFilterOperator, string> = {\n  after: \"after\",\n  before: \"before\",\n  \"in-range\": \"in range\",\n  is: \"is\",\n  \"is-any-of\": \"is any of\",\n  \"is-not\": \"is not\",\n  \"not-in-range\": \"not in range\",\n}\n// Relative cutoffs for the date filters, mirroring Extend's table filters.\nconst DATE_FILTER_PRESETS = [\n  \"1 day ago\",\n  \"3 days ago\",\n  \"1 week ago\",\n  \"1 month ago\",\n  \"3 months ago\",\n  \"6 months ago\",\n  \"1 year ago\",\n]\nfunction dateFilterPresetCutoff(preset: string) {\n  const date = new Date()\n  switch (preset) {\n    case \"1 day ago\":\n      date.setDate(date.getDate() - 1)\n      break\n    case \"3 days ago\":\n      date.setDate(date.getDate() - 3)\n      break\n    case \"1 week ago\":\n      date.setDate(date.getDate() - 7)\n      break\n    case \"1 month ago\":\n      date.setMonth(date.getMonth() - 1)\n      break\n    case \"3 months ago\":\n      date.setMonth(date.getMonth() - 3)\n      break\n    case \"6 months ago\":\n      date.setMonth(date.getMonth() - 6)\n      break\n    case \"1 year ago\":\n      date.setFullYear(date.getFullYear() - 1)\n      break\n    default: {\n      const parsed = Date.parse(preset)\n      if (!Number.isNaN(parsed)) return new Date(parsed)\n    }\n  }\n  return date\n}\n// Custom ranges store two ISO timestamps instead of a relative preset.\nfunction isCustomDateRangeValue(value: string[]) {\n  return (\n    value.length === 2 &&\n    value.every(\n      (entry) =>\n        !DATE_FILTER_PRESETS.includes(entry) && !Number.isNaN(Date.parse(entry))\n    )\n  )\n}\nfunction filterOperatorChoices(\n  filter: FileSystemFilter\n): FileSystemFilterOperator[] {\n  if (filter.type === \"fileType\") {\n    return filter.value.length > 1 ? [\"is-any-of\", \"is-not\"] : [\"is\", \"is-not\"]\n  }\n  if (isCustomDateRangeValue(filter.value)) return [\"in-range\", \"not-in-range\"]\n  return [\"before\", \"after\"]\n}\nfunction fileMatchesFilter(file: FileEntry, filter: FileSystemFilter) {\n  if (filter.value.length === 0) return true\n  if (filter.type === \"fileType\") {\n    const matches = filter.value.includes(mimeTypeForFile(file))\n    return filter.operator === \"is-not\" ? !matches : matches\n  }\n  const timestamp =\n    filter.type === \"dateCreated\" ? file.createdAt : file.updatedAt\n  const time = timestamp ? Date.parse(timestamp) : Number.NaN\n  if (Number.isNaN(time)) return false\n  if (filter.operator === \"in-range\" || filter.operator === \"not-in-range\") {\n    const from = Date.parse(filter.value[0])\n    const to = Date.parse(filter.value[1] ?? filter.value[0])\n    const isInRange = time >= from && time <= to\n    return filter.operator === \"not-in-range\" ? !isInRange : isInRange\n  }\n  const cutoff = dateFilterPresetCutoff(filter.value[0]).getTime()\n  return filter.operator === \"before\" ? time <= cutoff : time >= cutoff\n}\nfunction buildFileSystemIndex(items: FileSystemItem[]): FileSystemIndex {\n  const folders = new Map<string, FolderEntry>()\n  const files = new Map<string, FileEntry>()\n  const ensureFolderChain = (folderPath: string) => {\n    let path = normalizeFolderPath(folderPath)\n    while (path && !folders.has(path)) {\n      folders.set(path, {\n        kind: \"folder\",\n        name: pathName(path),\n        parentPath: pathParent(path),\n        path,\n      })\n      path = pathParent(path)\n    }\n  }\n  for (const item of items) {\n    if (item.kind === \"folder\") {\n      const path = normalizeFolderPath(item.path)\n      if (!path) continue\n      folders.set(path, {\n        ...item,\n        name: item.name ?? pathName(path),\n        parentPath: normalizeFolderPath(item.parentPath ?? pathParent(path)),\n        path,\n      })\n      ensureFolderChain(pathParent(path))\n    } else {\n      if (!item.path) continue\n      files.set(item.path, {\n        ...item,\n        key: item.key ?? item.path,\n        name: item.name ?? pathName(item.path),\n        parentPath: normalizeFolderPath(\n          item.parentPath ?? pathParent(item.path)\n        ),\n      })\n      ensureFolderChain(pathParent(item.path))\n    }\n  }\n  const children = new Map<string, FileSystemEntry[]>()\n  const pushChild = (entry: FileSystemEntry) => {\n    const siblings = children.get(entry.parentPath)\n    if (siblings) {\n      siblings.push(entry)\n    } else {\n      children.set(entry.parentPath, [entry])\n    }\n  }\n  for (const folder of folders.values()) pushChild(folder)\n  for (const file of files.values()) pushChild(file)\n  for (const siblings of children.values()) {\n    siblings.sort(compareEntryNames)\n  }\n  // Folders without an explicit modified date inherit their newest child's —\n  // object stores carry no folder metadata, yet the list view shows the\n  // column and the date sorts compare it. Deepest first (a descendant's path\n  // is always longer than its ancestor's) so dates propagate up the chain.\n  const foldersDeepestFirst = [...folders.values()].sort(\n    (left, right) => right.path.length - left.path.length\n  )\n  for (const folder of foldersDeepestFirst) {\n    if (folder.updatedAt) continue\n    let newestTime = Number.NEGATIVE_INFINITY\n    let newestValue: string | undefined\n    for (const child of children.get(folder.path) ?? []) {\n      const value = child.updatedAt ?? child.createdAt\n      const time = value ? Date.parse(value) : Number.NaN\n      if (!Number.isNaN(time) && time > newestTime) {\n        newestTime = time\n        newestValue = value\n      }\n    }\n    if (newestValue) folder.updatedAt = newestValue\n  }\n  return { children, files, folders }\n}\nfunction folderHasChildren(index: FileSystemIndex, folder: FolderEntry) {\n  return (\n    (index.children.get(folder.path)?.length ?? 0) > 0 ||\n    folder.hasChildren === true\n  )\n}\n// A single SVG source so the same glyph renders as a React element, inside the\n// @pierre/trees shadow DOM (via CSS url()), and stays pixel-identical in both.\nconst FOLDER_GLYPH_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 50\" width=\"64\" height=\"50\"><defs><linearGradient id=\"fs-folder-back\" x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#3dabf5\"/><stop offset=\"1\" stop-color=\"#1d84dd\"/></linearGradient><linearGradient id=\"fs-folder-front\" x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#7accfb\"/><stop offset=\"1\" stop-color=\"#37a0ef\"/></linearGradient></defs><path d=\"M5 10c0-3.31 2.69-6 6-6h10.9c1.6 0 3.13.7 4.18 1.9l1.5 1.73a3.5 3.5 0 0 0 2.64 1.22H54c2.76 0 5 2.24 5 5V40c0 3.87-3.13 7-7 7H12c-3.87 0-7-3.13-7-7V10Z\" fill=\"url(#fs-folder-back)\"/><path d=\"M5 15.5h54V40c0 3.87-3.13 7-7 7H12c-3.87 0-7-3.13-7-7V15.5Z\" fill=\"url(#fs-folder-front)\"/></svg>`\nconst FOLDER_GLYPH_DATA_URL = `data:image/svg+xml,${encodeURIComponent(FOLDER_GLYPH_SVG)}`\nfunction FileSystemFolderGlyph({ className }: { className?: string }) {\n  return (\n    // eslint-disable-next-line @next/next/no-img-element -- The folder glyph is an inline SVG data URL shared with the tree sprite.\n    <img\n      src={FOLDER_GLYPH_DATA_URL}\n      alt=\"\"\n      aria-hidden=\"true\"\n      draggable={false}\n      className={className}\n    />\n  )\n}\nfunction escapeXmlAttribute(value: string) {\n  return value\n    .replaceAll(\"&\", \"&amp;\")\n    .replaceAll(\"<\", \"&lt;\")\n    .replaceAll(\">\", \"&gt;\")\n    .replaceAll('\"', \"&quot;\")\n}\n// The @pierre/trees \"complete\" set — the full, colored suite with brand and\n// framework glyphs — ships as an SVG sprite. The list view tree consumes it\n// natively inside its shadow DOM; the icon, column, and gallery views render\n// the same sprite from the light DOM so every view falls back to the same\n// file-type icon when a file has no thumbnail.\nconst FILE_ICON_SPRITE_SHEET = getBuiltInSpriteSheet(\"complete\")\nconst { resolveIcon: resolveFileIcon } = createFileTreeIconResolver({\n  colored: true,\n  set: \"complete\",\n})\n// Per-token light/dark colors mirroring the palette the tree applies inside\n// its shadow DOM. Tokens without an entry (font, nextjs, stylelint) stay\n// muted-foreground there too.\nconst FILE_ICON_COLORS: Record<string, [light: string, dark: string]> = {\n  astro: [\"#a631be\", \"#d568ea\"],\n  babel: [\"#d5a910\", \"#ffd452\"],\n  bash: [\"#199f43\", \"#5ecc71\"],\n  biome: [\"#1a85d4\", \"#69b1ff\"],\n  bootstrap: [\"#693acf\", \"#9d6afb\"],\n  browserslist: [\"#d5a910\", \"#ffd452\"],\n  bun: [\"#594c5b\", \"#79697b\"],\n  c: [\"#1a85d4\", \"#69b1ff\"],\n  claude: [\"#d47628\", \"#ffa359\"],\n  cpp: [\"#1a85d4\", \"#69b1ff\"],\n  css: [\"#693acf\", \"#9d6afb\"],\n  database: [\"#a631be\", \"#d568ea\"],\n  default: [\"#84848a\", \"#adadb1\"],\n  docker: [\"#1a85d4\", \"#69b1ff\"],\n  eslint: [\"#693acf\", \"#9d6afb\"],\n  git: [\"#ff8c5b\", \"#d5512f\"],\n  go: [\"#1ca1c7\", \"#68cdf2\"],\n  graphql: [\"#d32a61\", \"#ff678d\"],\n  html: [\"#d47628\", \"#ffa359\"],\n  image: [\"#d32a61\", \"#ff678d\"],\n  javascript: [\"#d5a910\", \"#ffd452\"],\n  json: [\"#d47628\", \"#ffa359\"],\n  markdown: [\"#199f43\", \"#5ecc71\"],\n  mcp: [\"#17a5af\", \"#64d1db\"],\n  npm: [\"#d52c36\", \"#ff6762\"],\n  oxc: [\"#1ca1c7\", \"#68cdf2\"],\n  postcss: [\"#d52c36\", \"#ff6762\"],\n  prettier: [\"#17a5af\", \"#64d1db\"],\n  python: [\"#1a85d4\", \"#69b1ff\"],\n  react: [\"#1ca1c7\", \"#68cdf2\"],\n  ruby: [\"#d52c36\", \"#ff6762\"],\n  rust: [\"#d47628\", \"#ffa359\"],\n  sass: [\"#d32a61\", \"#ff678d\"],\n  svelte: [\"#d52c36\", \"#ff6762\"],\n  svg: [\"#d47628\", \"#ffa359\"],\n  svgo: [\"#199f43\", \"#5ecc71\"],\n  swift: [\"#d47628\", \"#ffa359\"],\n  table: [\"#17a5af\", \"#64d1db\"],\n  tailwind: [\"#1ca1c7\", \"#68cdf2\"],\n  terraform: [\"#693acf\", \"#9d6afb\"],\n  text: [\"#84848a\", \"#adadb1\"],\n  typescript: [\"#1a85d4\", \"#69b1ff\"],\n  vite: [\"#a631be\", \"#d568ea\"],\n  vscode: [\"#1a85d4\", \"#69b1ff\"],\n  vue: [\"#199f43\", \"#5ecc71\"],\n  wasm: [\"#693acf\", \"#9d6afb\"],\n  webpack: [\"#1a85d4\", \"#69b1ff\"],\n  yml: [\"#d52c36\", \"#ff6762\"],\n  zig: [\"#d47628\", \"#ffa359\"],\n  zip: [\"#d47628\", \"#ffa359\"],\n}\nfunction fileIconColorVariables(mode: 0 | 1) {\n  return Object.entries(FILE_ICON_COLORS)\n    .map(([token, colors]) => `--fs-file-icon-${token}: ${colors[mode]};`)\n    .join(\" \")\n}\n// The variables live on :root rather than the component root because the\n// filter menus and dialogs portal outside it; the --fs-file-icon-*\n// namespace keeps them collision-free. Thumbnail tiles keep a light\n// (paper) surface in dark mode, so icons inside them revert to the light\n// palette ([data-file-system-on-light]); selected rows sit on the primary\n// surface — the opposite of the mode's background — so icons there swap to\n// the opposite palette ([data-file-system-on-primary] in the light DOM,\n// --fs-selected-color-scheme for the tree's light-dark() colors inside its\n// shadow DOM).\nconst FILE_ICON_COLOR_CSS = `\n:root { ${fileIconColorVariables(0)} --fs-selected-color-scheme: dark; }\n.dark { ${fileIconColorVariables(1)} --fs-selected-color-scheme: light; }\n.dark [data-file-system-on-light] { ${fileIconColorVariables(0)} }\n[data-file-system-on-primary] { ${fileIconColorVariables(1)} }\n.dark [data-file-system-on-primary] { ${fileIconColorVariables(0)} }\n`\nfunction FileSystemIconSpriteSheet() {\n  return (\n    <>\n      <span\n        aria-hidden=\"true\"\n        className=\"hidden\"\n        dangerouslySetInnerHTML={{ __html: FILE_ICON_SPRITE_SHEET }}\n      />\n      <style>{FILE_ICON_COLOR_CSS}</style>\n    </>\n  )\n}\nfunction FileTypeIcon({\n  fileName,\n  className,\n}: {\n  fileName: string\n  className?: string\n}) {\n  const icon = resolveFileIcon(\"file-tree-icon-file\", fileName)\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox={icon.viewBox ?? \"0 0 16 16\"}\n      className={cn(\"shrink-0 text-muted-foreground\", className)}\n      style={\n        icon.token\n          ? {\n              color: `var(--fs-file-icon-${icon.token}, var(--color-muted-foreground))`,\n            }\n          : undefined\n      }\n    >\n      <use href={`#${icon.name}`} />\n    </svg>\n  )\n}\nfunction FileGenericPreview({ file }: { file: FileEntry }) {\n  const extension = fileExtension(file.name)\n  return (\n    <div\n      data-file-system-on-light=\"\"\n      className=\"flex size-full flex-col items-center justify-center gap-1.5 bg-white text-neutral-400 dark:bg-neutral-100\"\n    >\n      <FileTypeIcon fileName={file.name} className=\"size-1/3 min-h-4 min-w-4\" />\n      {extension ? (\n        <span className=\"text-[min(0.625rem,18cqw)] font-semibold tracking-wide uppercase\">\n          {extension}\n        </span>\n      ) : null}\n    </div>\n  )\n}\nfunction filePreviewUrls(file: FileSystemFileItem) {\n  if (file.previewImageUrls?.length) return file.previewImageUrls\n  return file.previewImageUrl ? [file.previewImageUrl] : []\n}\n// Mirrors @pierre/trees' query normalization so the toolbar search filters\n// the icon, column, and gallery views exactly like the list view tree:\n// trimmed, backslashes to slashes, lowercased, substring match on the path.\nfunction normalizeSearchQuery(value: string) {\n  const trimmed = value.trim()\n  if (!trimmed) return \"\"\n  return trimmed.replaceAll(\"\\\\\", \"/\").toLowerCase()\n}\n// Windowed rendering, the approach @pierre/trees uses for the list view:\n// with a fixed item stride only the items intersecting the viewport — plus\n// `overscan` on each side — are mounted, so views stay flat-cost at\n// thousands of entries. The window keeps a one-item margin before\n// recomputing (scrolling doesn't re-render per item) and that margin also\n// guarantees single-step keyboard moves land on a mounted neighbor.\nfunction useVirtualWindow({\n  count,\n  horizontal = false,\n  itemStride,\n  leadingPx = 0,\n  overscan = 8,\n  viewportRef,\n}: {\n  count: number\n  horizontal?: boolean\n  itemStride: number\n  leadingPx?: number\n  overscan?: number\n  viewportRef: React.RefObject<HTMLDivElement | null>\n}) {\n  const [window_, setWindow] = React.useState(() => ({\n    end: Math.min(count, overscan * 2),\n    start: 0,\n  }))\n  React.useLayoutEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || itemStride <= 0) return\n    const update = () => {\n      const scrollStart =\n        (horizontal ? viewport.scrollLeft : viewport.scrollTop) - leadingPx\n      const viewportSize = horizontal\n        ? viewport.clientWidth\n        : viewport.clientHeight\n      const firstVisible = Math.max(0, Math.floor(scrollStart / itemStride))\n      const lastVisible = Math.min(\n        count,\n        Math.ceil((scrollStart + viewportSize) / itemStride)\n      )\n      setWindow((previous) => {\n        if (\n          previous.end <= count &&\n          previous.start <= Math.max(0, firstVisible - 1) &&\n          previous.end >= Math.min(count, lastVisible + 1)\n        ) {\n          return previous\n        }\n        return {\n          end: Math.min(count, lastVisible + overscan),\n          start: Math.max(0, firstVisible - overscan),\n        }\n      })\n    }\n    update()\n    viewport.addEventListener(\"scroll\", update, { passive: true })\n    const observer =\n      typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(update)\n    observer?.observe(viewport)\n    return () => {\n      viewport.removeEventListener(\"scroll\", update)\n      observer?.disconnect()\n    }\n  }, [count, horizontal, itemStride, leadingPx, overscan, viewportRef])\n  return window_\n}\n// Scrolls the item at `index` into the viewport when it sits outside it —\n// virtualized views need this because off-window items have no DOM node to\n// call scrollIntoView on.\nfunction scrollIndexIntoView({\n  horizontal = false,\n  index,\n  itemSize,\n  itemStride,\n  leadingPx = 0,\n  viewport,\n}: {\n  horizontal?: boolean\n  index: number\n  itemSize: number\n  itemStride: number\n  leadingPx?: number\n  viewport: HTMLDivElement | null\n}) {\n  if (!viewport || index < 0) return\n  const start = leadingPx + index * itemStride\n  const end = start + itemSize\n  const scrollStart = horizontal ? viewport.scrollLeft : viewport.scrollTop\n  const viewportSize = horizontal ? viewport.clientWidth : viewport.clientHeight\n  let nextScrollStart: number | null = null\n  if (start < scrollStart) {\n    nextScrollStart = start\n  } else if (end > scrollStart + viewportSize) {\n    nextScrollStart = end - viewportSize\n  }\n  if (nextScrollStart === null) return\n  if (horizontal) {\n    viewport.scrollLeft = nextScrollStart\n  } else {\n    viewport.scrollTop = nextScrollStart\n  }\n}\nfunction FileVisual({\n  file,\n  className,\n  loadPreviewImageUrl,\n  pageable = false,\n  pageUrlCache,\n  previewAspectRatio,\n  previewClassName,\n  renderFilePreview,\n}: {\n  file: FileEntry\n  className?: string\n  loadPreviewImageUrl?: (\n    file: FileSystemFileItem,\n    pageIndex: number\n  ) => Promise<string | null>\n  /** Show a hover pager over multi-page thumbnails. */\n  pageable?: boolean\n  /**\n   * Shared `\"path#pageIndex\"` → URL cache so pages fetched by one pager\n   * (gallery stage, columns preview) are reused by every other instance.\n   */\n  pageUrlCache?: Map<string, string>\n  previewAspectRatio?: number\n  previewClassName?: string\n  renderFilePreview?: (file: FileSystemFileItem) => React.ReactNode\n}) {\n  const previewUrls = filePreviewUrls(file)\n  const canLoadLazily = pageable && Boolean(loadPreviewImageUrl)\n  const totalPages = Math.max(\n    previewUrls.length,\n    canLoadLazily ? (file.previewPageCount ?? 0) : 0\n  )\n  const [pageIndex, setPageIndex] = React.useState(0)\n  const [lazyPageUrls, setLazyPageUrls] = React.useState<\n    Record<number, string>\n  >({})\n  const clampedPageIndex = Math.min(pageIndex, Math.max(totalPages - 1, 0))\n  const previewUrl =\n    previewUrls[clampedPageIndex] ??\n    lazyPageUrls[clampedPageIndex] ??\n    pageUrlCache?.get(`${file.path}#${clampedPageIndex}`) ??\n    null\n  const resolvedAspectRatio = file.previewAspectRatio ?? previewAspectRatio\n  const isLazyPagePending =\n    canLoadLazily && !previewUrl && clampedPageIndex < totalPages\n  const fileRef = React.useRef(file)\n  React.useEffect(() => {\n    fileRef.current = file\n  })\n  const [previousFilePath, setPreviousFilePath] = React.useState(file.path)\n  if (!Object.is(previousFilePath, file.path)) {\n    setPreviousFilePath(file.path)\n    setPageIndex(0)\n    setLazyPageUrls({})\n  }\n  // Keyed by path (not object identity) so manifest churn doesn't re-request\n  // the page already being loaded.\n  React.useEffect(() => {\n    if (!isLazyPagePending || !loadPreviewImageUrl) return\n    let isCurrent = true\n    void loadPreviewImageUrl(fileRef.current, clampedPageIndex)\n      .then((url) => {\n        // Cache even when stale (page flipped away mid-load): the fetch is\n        // done, so let the next visit use it.\n        if (url) pageUrlCache?.set(`${file.path}#${clampedPageIndex}`, url)\n        if (isCurrent && url) {\n          setLazyPageUrls((previous) => ({\n            ...previous,\n            [clampedPageIndex]: url,\n          }))\n        }\n      })\n      .catch(() => {})\n    return () => {\n      isCurrent = false\n    }\n  }, [\n    clampedPageIndex,\n    file.path,\n    isLazyPagePending,\n    loadPreviewImageUrl,\n    pageUrlCache,\n  ])\n  const customPreview =\n    !previewUrl && !isLazyPagePending ? renderFilePreview?.(file) : null\n  const showPager = pageable && totalPages > 1\n  const thumbnail = (\n    <FileThumbnail\n      file={{ name: file.name, type: file.contentType ?? \"\" }}\n      className={cn(\"@container\", !showPager && className)}\n      previewAspectRatio={resolvedAspectRatio}\n      previewClassName={cn(\"bg-white dark:bg-neutral-100\", previewClassName)}\n      previewImageUrl={previewUrl ?? undefined}\n      isLoading={isLazyPagePending}\n      previewContent={\n        previewUrl || isLazyPagePending\n          ? undefined\n          : (customPreview ?? <FileGenericPreview file={file} />)\n      }\n    />\n  )\n  if (!showPager) return thumbnail\n  return (\n    <div className={cn(\"group/pager relative\", className)}>\n      {thumbnail}\n      <div className=\"absolute inset-x-0 bottom-1.5 flex items-center justify-center gap-1 opacity-0 transition-opacity group-focus-within/pager:opacity-100 group-hover/pager:opacity-100\">\n        <button\n          type=\"button\"\n          aria-label=\"Previous page\"\n          tabIndex={-1}\n          disabled={clampedPageIndex === 0}\n          onClick={(event) => {\n            event.stopPropagation()\n            setPageIndex((previous) => Math.max(0, previous - 1))\n          }}\n          onDoubleClick={(event) => event.stopPropagation()}\n          className=\"flex size-6 items-center justify-center rounded-md bg-background/80 text-foreground shadow-xs backdrop-blur-sm transition-colors outline-none hover:bg-background focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\"\n        >\n          <IconPlaceholder\n            lucide=\"ChevronLeft\"\n            tabler=\"IconChevronLeft\"\n            hugeicons=\"ArrowLeft01Icon\"\n            phosphor=\"CaretLeftIcon\"\n            remixicon=\"RiArrowLeftSLine\"\n            className=\"size-3.5\"\n          />\n        </button>\n        <span className=\"rounded-md bg-background/80 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground tabular-nums shadow-xs backdrop-blur-sm\">\n          {clampedPageIndex + 1}/{totalPages}\n        </span>\n        <button\n          type=\"button\"\n          aria-label=\"Next page\"\n          tabIndex={-1}\n          disabled={clampedPageIndex >= totalPages - 1}\n          onClick={(event) => {\n            event.stopPropagation()\n            setPageIndex((previous) => Math.min(totalPages - 1, previous + 1))\n          }}\n          onDoubleClick={(event) => event.stopPropagation()}\n          className=\"flex size-6 items-center justify-center rounded-md bg-background/80 text-foreground shadow-xs backdrop-blur-sm transition-colors outline-none hover:bg-background focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\"\n        >\n          <IconPlaceholder\n            lucide=\"ArrowRight\"\n            tabler=\"IconArrowRight\"\n            hugeicons=\"ArrowRight01Icon\"\n            phosphor=\"ArrowRightIcon\"\n            remixicon=\"RiArrowRightLine\"\n            className=\"size-3.5\"\n          />\n        </button>\n      </div>\n    </div>\n  )\n}\nconst VIEW_OPTIONS: Array<{\n  icon: React.ComponentType<InlineRegistryIconProps>\n  label: string\n  value: FileSystemView\n}> = [\n  { icon: GridViewGlyph, label: \"Grid\", value: \"icons\" },\n  { icon: LeftToRightListBulletGlyph, label: \"List\", value: \"list\" },\n  { icon: LayoutThreeColumnGlyph, label: \"Columns\", value: \"columns\" },\n  { icon: GalleryThumbnailsGlyph, label: \"Gallery\", value: \"gallery\" },\n]\nexport function FileSystem({\n  items,\n  className,\n  title = \"Files\",\n  defaultView = \"icons\",\n  view: viewProp,\n  onViewChange,\n  defaultPath = \"\",\n  onSelectionChange,\n  onFileOpen,\n  getFileUrl,\n  loadChildren,\n  loadPreviewImageUrl,\n  renderFilePreview,\n}: FileSystemProps) {\n  const [internalView, setInternalView] = React.useState(defaultView)\n  const view = viewProp ?? internalView\n  const setView = React.useCallback(\n    (nextView: FileSystemView) => {\n      setInternalView(nextView)\n      onViewChange?.(nextView)\n    },\n    [onViewChange]\n  )\n  const [loadedItems, setLoadedItems] = React.useState<FileSystemItem[]>([])\n  const allItems = React.useMemo(\n    () => (loadedItems.length ? [...items, ...loadedItems] : items),\n    [items, loadedItems]\n  )\n  const index = React.useMemo(() => buildFileSystemIndex(allItems), [allItems])\n  const [history, setHistory] = React.useState(() => ({\n    index: 0,\n    stack: [normalizeFolderPath(defaultPath)],\n  }))\n  const currentPath = history.stack[history.index] ?? \"\"\n  const canGoBack = history.index > 0\n  const canGoForward = history.index < history.stack.length - 1\n  const [selectedPath, setSelectedPath] = React.useState<string | null>(null)\n  const selectedEntry = React.useMemo(() => {\n    if (selectedPath === null) return null\n    return (\n      index.files.get(selectedPath) ?? index.folders.get(selectedPath) ?? null\n    )\n  }, [index, selectedPath])\n  const [searchInput, setSearchInput] = React.useState(\"\")\n  const searchInputRef = React.useRef<HTMLInputElement | null>(null)\n  const [isSearchExpanded, setIsSearchExpanded] = React.useState(false)\n  const searchQuery = normalizeSearchQuery(searchInput)\n  const isSearching = searchQuery.length > 0\n  const [sort, setSort] = React.useState(DEFAULT_SORT)\n  const [filters, setFilters] = React.useState<FileSystemFilter[]>([])\n  const hasActiveFilters = filters.length > 0\n  // Files must pass every active filter; folders stay visible through\n  // matching descendants, so the predicate only ever sees files.\n  const fileFilter = React.useMemo(() => {\n    if (filters.length === 0) return null\n    return (file: FileEntry) =>\n      filters.every((filter) => fileMatchesFilter(file, filter))\n  }, [filters])\n  // Paths that stay visible while searching or filtering: every file whose\n  // currentPath-relative path contains the query — the list view tree's\n  // hide-non-matches semantics — and that passes the filters, plus the\n  // ancestor folders leading to it. Folder names participate in search\n  // matches only when no filters are active; with filters, a folder is only\n  // as visible as the files inside it.\n  const visiblePaths = React.useMemo(() => {\n    if (!isSearching && !fileFilter) return null\n    const visible = new Set<string>()\n    const markVisible = (path: string) => {\n      while (path && path !== currentPath && !visible.has(path)) {\n        visible.add(path)\n        path = pathParent(path)\n      }\n    }\n    const matchesQuery = (path: string) =>\n      !isSearching ||\n      path.slice(currentPath.length).toLowerCase().includes(searchQuery)\n    for (const [path, file] of index.files) {\n      if (path === currentPath) continue\n      if (currentPath && !path.startsWith(currentPath)) continue\n      if (!matchesQuery(path)) continue\n      if (fileFilter && !fileFilter(file)) continue\n      markVisible(path)\n    }\n    if (!fileFilter) {\n      for (const path of index.folders.keys()) {\n        if (path === currentPath) continue\n        if (currentPath && !path.startsWith(currentPath)) continue\n        if (matchesQuery(path)) markVisible(path)\n      }\n    }\n    return visible\n  }, [currentPath, fileFilter, index, isSearching, searchQuery])\n  const visibleIndex = React.useMemo(() => {\n    if (!visiblePaths) return index\n    const children = new Map<string, FileSystemEntry[]>()\n    for (const [parentPath, parentChildren] of index.children) {\n      const visibleChildren = parentChildren.filter((entry) =>\n        visiblePaths.has(entry.path)\n      )\n      if (visibleChildren.length) children.set(parentPath, visibleChildren)\n    }\n    return { ...index, children }\n  }, [index, visiblePaths])\n  // Children re-sorted per the active sort; the default (name ascending)\n  // reuses the index's pre-sorted arrays untouched.\n  const sortedIndex = React.useMemo(() => {\n    if (\n      sort.key === DEFAULT_SORT.key &&\n      sort.direction === DEFAULT_SORT.direction\n    ) {\n      return visibleIndex\n    }\n    const children = new Map<string, FileSystemEntry[]>()\n    for (const [parentPath, parentChildren] of visibleIndex.children) {\n      children.set(\n        parentPath,\n        [...parentChildren].sort((left, right) =>\n          compareEntriesBySort(left, right, sort)\n        )\n      )\n    }\n    return { ...visibleIndex, children }\n  }, [sort, visibleIndex])\n  // The ref mirrors the state so re-selecting the same entry (e.g. the\n  // pointerdown + click pair the columns view emits per press) stays a\n  // no-op without widening the callback's dependencies.\n  const selectedPathRef = React.useRef<string | null>(null)\n  const selectEntry = React.useCallback(\n    (entry: FileSystemEntry | null) => {\n      const path = entry?.path ?? null\n      if (selectedPathRef.current === path) return\n      selectedPathRef.current = path\n      setSelectedPath(path)\n      onSelectionChange?.(entry)\n    },\n    [onSelectionChange]\n  )\n  // A query or filter change can hide the selected entry out from under the\n  // views.\n  React.useEffect(() => {\n    if (!visiblePaths || !selectedPath) return\n    if (!visiblePaths.has(selectedPath)) selectEntry(null)\n  }, [selectEntry, selectedPath, visiblePaths])\n  const applySortKey = React.useCallback((key: FileSystemSortKey) => {\n    setSort((previous) =>\n      previous.key === key\n        ? previous\n        : { direction: defaultSortDirection(key), key }\n    )\n  }, [])\n  // Column headers toggle the direction when the column is already active,\n  // like Finder.\n  const toggleSortColumn = React.useCallback((key: FileSystemSortKey) => {\n    setSort((previous) =>\n      previous.key === key\n        ? { direction: previous.direction === \"asc\" ? \"desc\" : \"asc\", key }\n        : { direction: defaultSortDirection(key), key }\n    )\n  }, [])\n  // Distinct MIME types across the loaded manifest, labeled for the filter\n  // menu; the first file seen per type lends its name to the option icon.\n  const fileTypeOptions = React.useMemo(() => {\n    const byMime = new Map<string, FileTypeFilterOption>()\n    for (const file of index.files.values()) {\n      const mime = mimeTypeForFile(file)\n      if (!byMime.has(mime)) {\n        // The leading-dot check keeps dotfiles (.gitignore) whole.\n        const dotIndex = file.name.lastIndexOf(\".\")\n        const extension =\n          dotIndex > 0 ? file.name.slice(dotIndex + 1).toLowerCase() : \"\"\n        byMime.set(mime, {\n          group: fileTypeFilterGroup(mime),\n          // A synthesized generic name, so files with branded icons\n          // (biome.json, next.config.ts, CLAUDE.md, …) don't lend them to\n          // the whole type; extensionless names keep their own icon\n          // (Dockerfile, Makefile).\n          iconFileName: extension ? `file.${extension}` : file.name,\n          label: MIME_TYPE_LABELS[mime] ?? mime,\n          mime,\n        })\n      }\n    }\n    return [...byMime.values()].sort((left, right) =>\n      left.label.localeCompare(right.label)\n    )\n  }, [index])\n  const filterIdRef = React.useRef(0)\n  const [dateRangeDialog, setDateRangeDialog] = React.useState<{\n    initialRange?: {\n      from: Date\n      to: Date\n    }\n    type: FileSystemDateFilterType\n  } | null>(null)\n  const toggleFileTypeFilterValue = React.useCallback(\n    (mime: string, checked: boolean) => {\n      const id = `filter-${++filterIdRef.current}`\n      setFilters((previous) => {\n        const existing = previous.find((filter) => filter.type === \"fileType\")\n        if (!existing) {\n          if (!checked) return previous\n          return [\n            ...previous,\n            {\n              id,\n              operator: \"is\" as const,\n              type: \"fileType\" as const,\n              value: [mime],\n            },\n          ]\n        }\n        const value = checked\n          ? [...new Set([...existing.value, mime])]\n          : existing.value.filter((entry) => entry !== mime)\n        if (value.length === 0) {\n          return previous.filter((filter) => filter !== existing)\n        }\n        // \"is\" and \"is any of\" track the value count; \"is not\" is unaffected.\n        const operator =\n          existing.operator === \"is\" || existing.operator === \"is-any-of\"\n            ? value.length > 1\n              ? (\"is-any-of\" as const)\n              : (\"is\" as const)\n            : existing.operator\n        return previous.map((filter) =>\n          filter === existing ? { ...filter, operator, value } : filter\n        )\n      })\n    },\n    []\n  )\n  const setDatePresetFilter = React.useCallback(\n    (type: FileSystemDateFilterType, preset: string) => {\n      const id = `filter-${++filterIdRef.current}`\n      setFilters((previous) => [\n        ...previous.filter((filter) => filter.type !== type),\n        { id, operator: \"after\", type, value: [preset] },\n      ])\n    },\n    []\n  )\n  // Editing an existing custom range seeds the dialog with its bounds.\n  const openDateRangeDialog = React.useCallback(\n    (type: FileSystemDateFilterType) => {\n      const existing = filters.find((filter) => filter.type === type)\n      setDateRangeDialog({\n        initialRange:\n          existing && isCustomDateRangeValue(existing.value)\n            ? {\n                from: new Date(existing.value[0]),\n                to: new Date(existing.value[1]),\n              }\n            : undefined,\n        type,\n      })\n    },\n    [filters]\n  )\n  const applyCustomDateRange = React.useCallback(\n    (type: FileSystemDateFilterType, from: Date, to: Date) => {\n      const id = `filter-${++filterIdRef.current}`\n      setFilters((previous) => {\n        const existing = previous.find((filter) => filter.type === type)\n        return [\n          ...previous.filter((filter) => filter.type !== type),\n          {\n            id,\n            operator:\n              existing?.operator === \"not-in-range\"\n                ? (\"not-in-range\" as const)\n                : (\"in-range\" as const),\n            type,\n            value: [from.toISOString(), to.toISOString()],\n          },\n        ]\n      })\n    },\n    []\n  )\n  // Below iPad width the view switcher collapses into a select and the sort\n  // select drops its label; below 560px the search input collapses into a\n  // popover, and below 360px the folder name is dropped too.\n  const rootRef = React.useRef<HTMLDivElement | null>(null)\n  const [headerLayout, setHeaderLayout] = React.useState<\n    \"full\" | \"compact\" | \"minimal\"\n  >(\"full\")\n  const [isBelowIpadWidth, setIsBelowIpadWidth] = React.useState(false)\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root || typeof ResizeObserver === \"undefined\") return\n    const applyWidth = (width: number | undefined) => {\n      if (width === undefined) return\n      setHeaderLayout(\n        width < 360 ? \"minimal\" : width < 560 ? \"compact\" : \"full\"\n      )\n      setIsBelowIpadWidth(width < IPAD_MIN_WIDTH)\n    }\n    const observer = new ResizeObserver((observerEntries) =>\n      applyWidth(observerEntries[0]?.contentRect.width)\n    )\n    // Measure synchronously so the first painted layout is already correct;\n    // the observer then tracks resizes.\n    applyWidth(root.clientWidth)\n    observer.observe(root)\n    return () => observer.disconnect()\n  }, [])\n  const requestedFoldersRef = React.useRef(new Set<string>())\n  const [loadingFolders, setLoadingFolders] = React.useState<Set<string>>(\n    () => new Set()\n  )\n  const ensureChildren = React.useCallback(\n    (folderPath: string) => {\n      if (!loadChildren) return\n      const folder = index.folders.get(folderPath)\n      if (!folder?.hasChildren) return\n      if (index.children.get(folderPath)?.length) return\n      if (requestedFoldersRef.current.has(folderPath)) return\n      requestedFoldersRef.current.add(folderPath)\n      setLoadingFolders((previous) => new Set(previous).add(folderPath))\n      void (async () => {\n        try {\n          let cursor: string | null = null\n          do {\n            const result = await loadChildren({ cursor, path: folderPath })\n            if (result.items.length) {\n              setLoadedItems((previous) => [...previous, ...result.items])\n            }\n            cursor = result.nextCursor ?? null\n          } while (cursor)\n        } catch {\n          requestedFoldersRef.current.delete(folderPath)\n        } finally {\n          setLoadingFolders((previous) => {\n            const next = new Set(previous)\n            next.delete(folderPath)\n            return next\n          })\n        }\n      })()\n    },\n    [index, loadChildren]\n  )\n  const navigateTo = React.useCallback(\n    (folderPath: string) => {\n      const path = normalizeFolderPath(folderPath)\n      setHistory((previous) => {\n        if (previous.stack[previous.index] === path) return previous\n        const stack = [...previous.stack.slice(0, previous.index + 1), path]\n        return { index: stack.length - 1, stack }\n      })\n      // Navigation exits search, like Finder.\n      setSearchInput(\"\")\n      selectEntry(null)\n      ensureChildren(path)\n    },\n    [ensureChildren, selectEntry]\n  )\n  React.useEffect(() => {\n    ensureChildren(currentPath)\n  }, [currentPath, ensureChildren])\n  // Navigation unmounts the focused row, dropping focus to <body> and killing\n  // the ⌘ shortcuts; reclaim focus onto the component root when that happens.\n  const previousPathRef = React.useRef(currentPath)\n  React.useEffect(() => {\n    if (previousPathRef.current === currentPath) {\n      return\n    }\n    previousPathRef.current = currentPath\n    const root = rootRef.current\n    if (root && document.activeElement === document.body) {\n      root.focus({ preventScroll: true })\n    }\n  }, [currentPath])\n  const [openedFile, setOpenedFile] = React.useState<{\n    file: FileEntry\n    kind: FileSystemViewerKind\n    url: string\n  } | null>(null)\n  // Component-lifetime caches shared by every view and the open dialog:\n  // resolved (e.g. presigned) URLs keyed by path, and lazily loaded page\n  // thumbnails keyed by `\"path#pageIndex\"`. Each resolution happens once no\n  // matter how often the user revisits a file or switches views; stable\n  // URLs also keep the browser's HTTP cache valid for fetched content.\n  // Lazy state (never set) rather than refs: the Maps are passed down\n  // during render, which the rules of React disallow for ref reads.\n  const [resolvedUrlCache] = React.useState(() => new Map<string, string>())\n  const [pageUrlCache] = React.useState(() => new Map<string, string>())\n  // The keep-alive preview pool. Recently shown documents stay mounted so\n  // returning to one — in the gallery stage or the viewer dialog — skips\n  // the download and parse work instead of repeating it behind a spinner.\n  // Each pooled path renders through a portal into a stable detached <div>\n  // created once per path and never swapped (React remounts a portal's\n  // children when its container changes); a layout effect reparents that\n  // div into whichever host currently shows the file: the gallery's stage\n  // wrapper or the open dialog. Imperative appendChild keeps the mounted\n  // viewer (and its parsed document) alive across every move. Pooled paths\n  // without a current host stay mounted but DETACHED from the DOM — a\n  // detached subtree costs no layout, paint, or style-recalc work, so idle\n  // pool members never slow down interactions in the visible viewer.\n  const [stagePool, setStagePool] = React.useState<string[]>([])\n  const [stageRecency] = React.useState(() => new Map<string, number>())\n  const stageClockRef = React.useRef(0)\n  const [stageContainers] = React.useState(\n    () => new Map<string, HTMLDivElement>()\n  )\n  const [stageHosts] = React.useState(() => new Map<string, HTMLElement>())\n  const [, bumpStageHosts] = React.useState(0)\n  // Bumped on every admission so the attach set recomputes when recency\n  // changes without a pool membership change.\n  const [stageVersion, setStageVersion] = React.useState(0)\n  const [dialogStageHost, setDialogStageHost] =\n    React.useState<HTMLElement | null>(null)\n  const registerStageHost = React.useCallback(\n    (path: string, element: HTMLElement | null) => {\n      if (element) {\n        if (stageHosts.get(path) === element) return\n        stageHosts.set(path, element)\n      } else {\n        if (!stageHosts.has(path)) return\n        stageHosts.delete(path)\n      }\n      bumpStageHosts((version) => version + 1)\n    },\n    [stageHosts]\n  )\n  const dialogStageHostRef = React.useCallback(\n    (element: HTMLDivElement | null) => setDialogStageHost(element),\n    []\n  )\n  // Admits a file into the pool (idempotent), evicting the least recently\n  // admitted path beyond the cap. The pool array keeps insertion order —\n  // reordering would churn the host registrations — so recency lives in a\n  // separate map; the version bump re-renders so the attach set below\n  // tracks recency even when pool membership is unchanged.\n  const poolStagePath = React.useCallback(\n    (path: string) => {\n      if (!index.files.has(path)) return\n      if (!stageContainers.has(path)) {\n        const container = document.createElement(\"div\")\n        // Layout/paint containment keeps work inside one preview from\n        // invalidating the rest of the page (and vice versa).\n        container.className =\n          \"flex size-full min-h-0 min-w-0 items-center justify-center contain-layout contain-paint\"\n        stageContainers.set(path, container)\n      }\n      stageRecency.set(path, ++stageClockRef.current)\n      setStageVersion((version) => version + 1)\n      setStagePool((previous) => {\n        if (previous.includes(path)) return previous\n        const next = [...previous, path]\n        if (next.length <= GALLERY_STAGE_POOL_SIZE) return next\n        let evicted = next[0]\n        for (const candidate of next) {\n          if (candidate === path) continue\n          if (\n            (stageRecency.get(candidate) ?? 0) <\n            (stageRecency.get(evicted) ?? 0)\n          ) {\n            evicted = candidate\n          }\n        }\n        return next.filter((candidate) => candidate !== evicted)\n      })\n    },\n    [index, stageContainers, stageRecency]\n  )\n  const dialogStagePath =\n    openedFile !== null && openedFile.kind !== \"image\"\n      ? openedFile.file.path\n      : null\n  // Only the most recently shown stages stay attached to the DOM, so\n  // rotating among a few files stays instant while older pool members wait\n  // detached at zero rendering cost. Memoized so host ref callbacks\n  // downstream stay referentially stable — recomputing every render would\n  // re-register hosts in a loop.\n  const attachedStagePaths = React.useMemo(() => {\n    void stageVersion\n    const attached = [...stagePool]\n      .sort((a, b) => (stageRecency.get(b) ?? 0) - (stageRecency.get(a) ?? 0))\n      .slice(0, GALLERY_STAGE_ATTACHED_COUNT)\n    if (\n      dialogStagePath &&\n      stagePool.includes(dialogStagePath) &&\n      !attached.includes(dialogStagePath)\n    ) {\n      attached.push(dialogStagePath)\n    }\n    return attached\n  }, [dialogStagePath, stagePool, stageRecency, stageVersion])\n  // Reparent each pooled container to its current host. No dependency\n  // array: host registration mutates maps in place, so the cheap loop\n  // (pool ≤ GALLERY_STAGE_POOL_SIZE) runs every commit instead of chasing\n  // every mutation source.\n  React.useLayoutEffect(() => {\n    for (const [path, container] of stageContainers) {\n      if (!stagePool.includes(path)) {\n        // Evicted — React already unmounted the portal's children.\n        container.remove()\n        stageContainers.delete(path)\n        continue\n      }\n      if (dialogStagePath === path) {\n        // Leave the container in place until the dialog host mounts.\n        if (dialogStageHost && container.parentElement !== dialogStageHost) {\n          dialogStageHost.appendChild(container)\n        }\n        continue\n      }\n      const target = attachedStagePaths.includes(path)\n        ? (stageHosts.get(path) ?? null)\n        : null\n      if (!target) {\n        if (container.parentElement) container.remove()\n      } else if (container.parentElement !== target) {\n        target.appendChild(container)\n      }\n    }\n  })\n  const openFile = React.useCallback(\n    (file: FileEntry) => {\n      void (async () => {\n        let url = file.url ?? resolvedUrlCache.get(file.path) ?? null\n        if (!url && getFileUrl) {\n          try {\n            url = await getFileUrl(file)\n            if (url) resolvedUrlCache.set(file.path, url)\n          } catch {\n            url = null\n          }\n        }\n        if (onFileOpen) {\n          onFileOpen(file, url)\n          return\n        }\n        const kind = viewerKindForFile(file)\n        if (kind && url) {\n          // Pool the file so the dialog reuses an already-mounted preview\n          // (and the gallery inherits the live viewer after it closes).\n          poolStagePath(file.path)\n          setOpenedFile({ file, kind, url })\n        } else if (url && typeof window !== \"undefined\") {\n          window.open(url, \"_blank\", \"noopener,noreferrer\")\n        }\n      })()\n    },\n    [getFileUrl, onFileOpen, poolStagePath, resolvedUrlCache]\n  )\n  const openEntry = React.useCallback(\n    (entry: FileSystemEntry) => {\n      if (entry.kind === \"folder\") {\n        navigateTo(entry.path)\n      } else {\n        openFile(entry)\n      }\n    },\n    [navigateTo, openFile]\n  )\n  // Selecting a lazy folder (columns view, keyboard nav) prefetches children.\n  const selectAndPrefetchEntry = React.useCallback(\n    (entry: FileSystemEntry | null) => {\n      selectEntry(entry)\n      if (entry?.kind === \"folder\") ensureChildren(entry.path)\n    },\n    [ensureChildren, selectEntry]\n  )\n  const goBack = React.useCallback(() => {\n    setHistory((previous) => ({\n      ...previous,\n      index: Math.max(0, previous.index - 1),\n    }))\n    setSearchInput(\"\")\n    selectEntry(null)\n  }, [selectEntry])\n  const goForward = React.useCallback(() => {\n    setHistory((previous) => ({\n      ...previous,\n      index: Math.min(previous.stack.length - 1, previous.index + 1),\n    }))\n    setSearchInput(\"\")\n    selectEntry(null)\n  }, [selectEntry])\n  const currentEntries = sortedIndex.children.get(currentPath) ?? []\n  const currentFolderName =\n    currentPath === \"\" ? title : pathName(currentPath) || title\n  const isLoadingCurrentFolder = loadingFolders.has(currentPath)\n  // The list view tree saves its expanded folders here when it unmounts\n  // (view switches, navigation) so returning to the list view — or to a\n  // previously visited folder — restores the same disclosure state.\n  const treeExpansionRef = React.useRef(new Map<string, readonly string[]>())\n  const viewProps: FileSystemViewProps = {\n    attachedStagePaths,\n    currentPath,\n    entries: currentEntries,\n    fileFilter,\n    getFileUrl,\n    index: sortedIndex,\n    loadPreviewImageUrl,\n    loadingFolders,\n    onOpen: openEntry,\n    onSelect: selectAndPrefetchEntry,\n    onSortColumnClick: toggleSortColumn,\n    pageUrlCache,\n    poolStagePath,\n    registerStageHost,\n    renderFilePreview,\n    searchQuery,\n    selectedEntry,\n    selectedPath,\n    sort,\n    treeExpansionRef,\n  }\n  const openedFileName = openedFile\n    ? (openedFile.file.name ?? openedFile.file.path)\n    : \"\"\n  const activeViewOption = VIEW_OPTIONS.find((option) => option.value === view)\n  const viewerCloseToolbarAction = (\n    <DialogClose\n      render={\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon-sm\"\n          aria-label=\"Close preview\"\n        >\n          <IconPlaceholder\n            lucide=\"X\"\n            tabler=\"IconX\"\n            hugeicons=\"Cancel01Icon\"\n            phosphor=\"XIcon\"\n            remixicon=\"RiCloseLine\"\n            className=\"size-4\"\n          />\n        </Button>\n      }\n    ></DialogClose>\n  )\n  return (\n    <div\n      ref={rootRef}\n      tabIndex={-1}\n      data-slot=\"file-system\"\n      onKeyDown={(event) => {\n        // ⌘F focuses the toolbar search while focus is inside the component.\n        if ((event.metaKey || event.ctrlKey) && event.key === \"f\") {\n          event.preventDefault()\n          setIsSearchExpanded(true)\n          searchInputRef.current?.focus()\n        }\n      }}\n      className={cn(\n        \"flex h-[480px] min-h-0 flex-col overflow-hidden rounded-xl border bg-background text-foreground outline-none\",\n        className\n      )}\n    >\n      <FileSystemIconSpriteSheet />\n      <div className=\"relative grid h-12 shrink-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2 border-b bg-muted/40 px-2\">\n        <div className=\"flex min-w-0 items-center gap-0.5\">\n          <button\n            type=\"button\"\n            aria-label=\"Back\"\n            title=\"Back\"\n            disabled={!canGoBack}\n            onClick={goBack}\n            className=\"flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\"\n          >\n            <IconPlaceholder\n              lucide=\"ChevronLeft\"\n              tabler=\"IconChevronLeft\"\n              hugeicons=\"ArrowLeft01Icon\"\n              phosphor=\"CaretLeftIcon\"\n              remixicon=\"RiArrowLeftSLine\"\n              className=\"size-4.5\"\n            />\n          </button>\n          <button\n            type=\"button\"\n            aria-label=\"Forward\"\n            title=\"Forward\"\n            disabled={!canGoForward}\n            onClick={goForward}\n            className=\"flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\"\n          >\n            <IconPlaceholder\n              lucide=\"ArrowRight\"\n              tabler=\"IconArrowRight\"\n              hugeicons=\"ArrowRight01Icon\"\n              phosphor=\"ArrowRightIcon\"\n              remixicon=\"RiArrowRightLine\"\n              className=\"size-4.5\"\n            />\n          </button>\n          {headerLayout !== \"minimal\" ? (\n            <span className=\"ml-1.5 truncate text-sm font-semibold\">\n              {currentFolderName}\n            </span>\n          ) : null}\n        </div>\n        {headerLayout !== \"full\" || isBelowIpadWidth ? (\n          <Select\n            value={view}\n            onValueChange={(value) => setView(value as FileSystemView)}\n          >\n            <SelectTrigger\n              size=\"sm\"\n              aria-label=\"View\"\n              // Icon-only like the sort select: sheds the base min-width to\n              // hug icon + chevron at the filter button's 28px height.\n              className=\"h-7 min-h-7 w-auto min-w-0 [&_svg]:size-4\"\n            >\n              <SelectValue>\n                {activeViewOption ? (\n                  <activeViewOption.icon className=\"size-4\" />\n                ) : null}\n              </SelectValue>\n            </SelectTrigger>\n            <SelectContent>\n              {VIEW_OPTIONS.map((option) => (\n                <SelectItem key={option.value} value={option.value}>\n                  <span className=\"flex items-center gap-2\">\n                    <option.icon className=\"size-4\" />\n                    {option.label}\n                  </span>\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        ) : (\n          <Tabs\n            value={view}\n            onValueChange={(value) => setView(value as FileSystemView)}\n            className=\"gap-0\"\n          >\n            <TabsList className=\"h-8 p-0.5\">\n              {VIEW_OPTIONS.map((option) => (\n                <TabsTrigger\n                  key={option.value}\n                  value={option.value}\n                  aria-label={`${option.label} view`}\n                  title={option.label}\n                  className=\"h-7 grow-0 px-2.5 sm:h-7\"\n                >\n                  <option.icon className=\"size-4\" />\n                </TabsTrigger>\n              ))}\n            </TabsList>\n          </Tabs>\n        )}\n        <div className=\"flex min-w-0 items-center justify-end gap-1\">\n          <FileSystemSortSelect\n            layout={headerLayout}\n            onKeyChange={applySortKey}\n            showLabel={!isBelowIpadWidth}\n            sort={sort}\n          />\n          <FileSystemFilterMenu\n            fileTypeOptions={fileTypeOptions}\n            filters={filters}\n            onOpenCustomRange={openDateRangeDialog}\n            onSelectDatePreset={setDatePresetFilter}\n            onToggleFileType={toggleFileTypeFilterValue}\n          />\n          <FileSystemSearchField\n            inputRef={searchInputRef}\n            isExpanded={isSearchExpanded}\n            layout={headerLayout}\n            onExpandedChange={setIsSearchExpanded}\n            onValueChange={setSearchInput}\n            value={searchInput}\n          />\n        </div>\n      </div>\n      {hasActiveFilters ? (\n        <div className=\"flex shrink-0 flex-wrap items-center gap-1 border-b bg-muted/20 px-2 py-1.5 text-xs text-muted-foreground\">\n          {filters.map((filter) => {\n            const dateFilterType =\n              filter.type === \"fileType\" ? null : filter.type\n            return (\n              <FileSystemFilterPill\n                key={filter.id}\n                fileTypeOptions={fileTypeOptions}\n                filter={filter}\n                onOpenCustomRange={\n                  dateFilterType\n                    ? () => openDateRangeDialog(dateFilterType)\n                    : undefined\n                }\n                onOperatorChange={(operator) =>\n                  setFilters((previous) =>\n                    previous.map((entry) =>\n                      entry.id === filter.id ? { ...entry, operator } : entry\n                    )\n                  )\n                }\n                onRemove={() =>\n                  setFilters((previous) =>\n                    previous.filter((entry) => entry.id !== filter.id)\n                  )\n                }\n                onSelectDatePreset={(preset) =>\n                  setFilters((previous) =>\n                    previous.map((entry) =>\n                      entry.id === filter.id\n                        ? {\n                            ...entry,\n                            operator:\n                              entry.operator === \"before\" ||\n                              entry.operator === \"after\"\n                                ? entry.operator\n                                : \"after\",\n                            value: [preset],\n                          }\n                        : entry\n                    )\n                  )\n                }\n                onToggleFileType={toggleFileTypeFilterValue}\n              />\n            )\n          })}\n          <button\n            type=\"button\"\n            onClick={() => setFilters([])}\n            className=\"rounded-md px-1.5 py-0.5 transition-colors outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n          >\n            Clear\n          </button>\n        </div>\n      ) : null}\n      <div className=\"relative min-h-0 flex-1\">\n        {isLoadingCurrentFolder && currentEntries.length === 0 ? (\n          <FileSystemEmptyState label=\"Loading…\" isLoading />\n        ) : currentEntries.length === 0 &&\n          (view !== \"columns\" || isSearching || hasActiveFilters) ? (\n          <FileSystemEmptyState\n            label={\n              isSearching\n                ? `No results for “${searchInput.trim()}”`\n                : hasActiveFilters\n                  ? \"No items match the active filters\"\n                  : \"This folder is empty\"\n            }\n          />\n        ) : view === \"icons\" ? (\n          <FileSystemIconsView {...viewProps} />\n        ) : view === \"list\" ? (\n          <FileSystemListView {...viewProps} />\n        ) : view === \"columns\" ? (\n          <FileSystemColumnsView {...viewProps} />\n        ) : (\n          <FileSystemGalleryView {...viewProps} />\n        )}\n      </div>\n      <div\n        aria-live=\"polite\"\n        className=\"flex h-7 shrink-0 items-center justify-center gap-1 border-t bg-muted/40 px-3 text-xs text-muted-foreground\"\n      >\n        <span>\n          {currentEntries.length}{\" \"}\n          {isSearching\n            ? currentEntries.length === 1\n              ? \"result\"\n              : \"results\"\n            : currentEntries.length === 1\n              ? \"item\"\n              : \"items\"}\n        </span>\n        {selectedEntry ? <span>· “{selectedEntry.name}” selected</span> : null}\n      </div>\n      <Dialog\n        open={openedFile !== null}\n        onOpenChange={(open) => {\n          if (!open) setOpenedFile(null)\n        }}\n      >\n        {openedFile ? (\n          <DialogContent\n            className={cn(\n              \"overflow-hidden p-0\",\n              VIEWER_DIALOG_CLASSNAMES[openedFile.kind]\n            )}\n            showCloseButton={openedFile.kind === \"image\"}\n          >\n            <DialogTitle className=\"sr-only\">{openedFileName}</DialogTitle>\n            {openedFile.kind === \"image\" ? (\n              // eslint-disable-next-line @next/next/no-img-element -- File previews render caller-provided URLs that may be object or presigned URLs.\n              <img\n                src={openedFile.url}\n                alt={openedFileName}\n                className=\"max-h-[88vh] w-auto max-w-full rounded-2xl object-contain\"\n              />\n            ) : (\n              // The pooled preview reparents into this host (see the layout\n              // effect above), so a viewer the gallery already loaded\n              // carries over live instead of remounting behind a loading\n              // state.\n              <div\n                ref={dialogStageHostRef}\n                className=\"flex h-full min-h-0 flex-1 flex-col\"\n              />\n            )}\n          </DialogContent>\n        ) : null}\n        {/* The pooled previews. Rendered inside <Dialog> so the dialog\n            variant's close toolbar button keeps its context; each portal's\n            container never changes, the container's parent does. */}\n        {stagePool.map((path) => {\n          const file = index.files.get(path)\n          const container = stageContainers.get(path)\n          if (!file || !container) return null\n          const isOpenedInDialog =\n            openedFile !== null &&\n            openedFile.kind !== \"image\" &&\n            openedFile.file.path === path\n          return createPortal(\n            <FileSystemGalleryStage\n              file={file}\n              getFileUrl={getFileUrl}\n              loadPreviewImageUrl={loadPreviewImageUrl}\n              pageUrlCache={pageUrlCache}\n              renderFilePreview={renderFilePreview}\n              toolbarActions={\n                isOpenedInDialog ? viewerCloseToolbarAction : undefined\n              }\n              urlCache={resolvedUrlCache}\n              variant={isOpenedInDialog ? \"dialog\" : \"stage\"}\n            />,\n            container,\n            path\n          )\n        })}\n      </Dialog>\n      {dateRangeDialog ? (\n        <FileSystemDateRangeDialog\n          initialRange={dateRangeDialog.initialRange}\n          onApply={(from, to) => {\n            applyCustomDateRange(dateRangeDialog.type, from, to)\n            setDateRangeDialog(null)\n          }}\n          onClose={() => setDateRangeDialog(null)}\n        />\n      ) : null}\n    </div>\n  )\n}\n// Shared style for the ghost icon buttons in the toolbar.\nconst TOOLBAR_ICON_BUTTON_CLASSNAME =\n  \"flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n// macOS Finder-style toolbar search. At the full layout it sits inline in\n// the header's right column; at compact widths it collapses into a ghost\n// icon button that opens the input in a popover (a dot marks the button\n// while a query keeps filtering the views).\nfunction FileSystemSearchField({\n  inputRef,\n  isExpanded,\n  layout,\n  onExpandedChange,\n  onValueChange,\n  value,\n}: {\n  inputRef: React.RefObject<HTMLInputElement | null>\n  isExpanded: boolean\n  layout: \"full\" | \"compact\" | \"minimal\"\n  onExpandedChange: (isExpanded: boolean) => void\n  onValueChange: (value: string) => void\n  value: string\n}) {\n  const isInline = layout === \"full\"\n  React.useEffect(() => {\n    if (!isInline && isExpanded) inputRef.current?.focus()\n  }, [inputRef, isExpanded, isInline])\n  const input = (\n    <div\n      className={cn(\n        \"relative flex h-7 min-w-0 flex-1 items-center rounded-lg border border-input bg-popover text-sm text-foreground shadow-xs/5 transition-shadow outline-none not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-focus-within:before:shadow-[0_1px_--theme(--color-black/4%)] focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-1 focus-within:ring-offset-background dark:bg-input/32 dark:not-focus-within:before:shadow-[0_-1px_--theme(--color-white/6%)]\",\n        isInline && \"max-w-56\"\n      )}\n    >\n      <IconPlaceholder\n        lucide=\"Search\"\n        tabler=\"IconSearch\"\n        hugeicons=\"Search01Icon\"\n        phosphor=\"MagnifyingGlassIcon\"\n        remixicon=\"RiSearchLine\"\n        className=\"pointer-events-none absolute left-2 size-3.5 text-muted-foreground\"\n      />\n      <input\n        ref={inputRef}\n        type=\"text\"\n        role=\"searchbox\"\n        aria-label=\"Search files\"\n        placeholder=\"Search\"\n        value={value}\n        onChange={(event) => onValueChange(event.target.value)}\n        onKeyDown={(event) => {\n          if (event.key !== \"Escape\") return\n          event.preventDefault()\n          event.stopPropagation()\n          if (value) {\n            onValueChange(\"\")\n          } else {\n            onExpandedChange(false)\n            event.currentTarget.blur()\n          }\n        }}\n        className=\"h-full w-full min-w-0 rounded-[inherit] bg-transparent pr-6 pl-7 outline-none placeholder:text-muted-foreground\"\n      />\n      {value ? (\n        <button\n          type=\"button\"\n          aria-label=\"Clear search\"\n          onClick={() => {\n            onValueChange(\"\")\n            inputRef.current?.focus()\n          }}\n          className=\"absolute right-1 flex size-5 items-center justify-center rounded-sm text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n        >\n          <IconPlaceholder\n            lucide=\"X\"\n            tabler=\"IconX\"\n            hugeicons=\"Cancel01Icon\"\n            phosphor=\"XIcon\"\n            remixicon=\"RiCloseLine\"\n            className=\"size-3\"\n          />\n        </button>\n      ) : null}\n    </div>\n  )\n  if (isInline) {\n    // A fixed basis (not flex-1) keeps the whole toolbar cluster packed\n    // against the header's right edge; the input shrinks first when the\n    // header tightens.\n    return <div className=\"flex w-56 min-w-32 items-center\">{input}</div>\n  }\n  return (\n    <Popover open={isExpanded} onOpenChange={onExpandedChange}>\n      <PopoverTrigger\n        render={\n          <button\n            type=\"button\"\n            aria-label=\"Search\"\n            title=\"Search\"\n            className={cn(TOOLBAR_ICON_BUTTON_CLASSNAME, \"relative\")}\n          >\n            <IconPlaceholder\n              lucide=\"Search\"\n              tabler=\"IconSearch\"\n              hugeicons=\"Search01Icon\"\n              phosphor=\"MagnifyingGlassIcon\"\n              remixicon=\"RiSearchLine\"\n              className=\"size-4\"\n            />\n            {value ? (\n              <span className=\"absolute top-1 right-1 size-1.5 rounded-full bg-primary\" />\n            ) : null}\n          </button>\n        }\n      ></PopoverTrigger>\n      <PopoverContent align=\"end\" sideOffset={6} className=\"w-64 p-1\">\n        {input}\n      </PopoverContent>\n    </Popover>\n  )\n}\n// Toolbar \"sort by\" select. The full layout shows the active key's label; at\n// compact widths the trigger collapses to the sort glyph + chevron.\nfunction FileSystemSortSelect({\n  layout,\n  onKeyChange,\n  showLabel,\n  sort,\n}: {\n  layout: \"full\" | \"compact\" | \"minimal\"\n  onKeyChange: (key: FileSystemSortKey) => void\n  showLabel: boolean\n  sort: FileSystemSortState\n}) {\n  const activeOption = SORT_OPTIONS.find((option) => option.key === sort.key)\n  return (\n    <Select\n      value={sort.key}\n      onValueChange={(value) => onKeyChange(value as FileSystemSortKey)}\n    >\n      <SelectTrigger\n        size=\"sm\"\n        aria-label=\"Sort by\"\n        title=\"Sort by\"\n        className=\"h-7 min-h-7 w-auto min-w-0 shrink-0 [&_svg]:size-4\"\n      >\n        <SelectValue>\n          <span className=\"flex items-center gap-1.5\">\n            <IconPlaceholder\n              lucide=\"ArrowUpDown\"\n              tabler=\"IconArrowsUpDown\"\n              hugeicons=\"ArrowUpDownIcon\"\n              phosphor=\"ArrowsDownUpIcon\"\n              remixicon=\"RiArrowUpDownLine\"\n              className=\"size-4\"\n            />\n            {layout === \"full\" && showLabel ? activeOption?.triggerLabel : null}\n          </span>\n        </SelectValue>\n      </SelectTrigger>\n      <SelectContent align=\"end\" alignItemWithTrigger={false}>\n        {SORT_OPTIONS.map((option) => (\n          <SelectItem key={option.key} value={option.key}>\n            {option.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  )\n}\n// Searchable file-type list (cmdk) rendered inside a menu popup, so the\n// long MIME list can be filtered by typing. Selection toggles stay open for\n// multi-select; ArrowUp/Down and Enter come from cmdk's combobox semantics.\nfunction FileSystemFileTypeCommand({\n  checkedMimes,\n  onToggle,\n  options,\n}: {\n  checkedMimes: string[]\n  onToggle: (mime: string, checked: boolean) => void\n  options: FileTypeFilterOption[]\n}) {\n  const inputRef = React.useRef<HTMLInputElement | null>(null)\n  // The menu focuses its popup when it opens; pull focus into the search\n  // field so typing filters immediately.\n  React.useEffect(() => {\n    const frame = requestAnimationFrame(() => inputRef.current?.focus())\n    return () => cancelAnimationFrame(frame)\n  }, [])\n  return (\n    <Command\n      // -m-1 spans the menu viewport's built-in padding so the search\n      // field's bottom border runs edge to edge.\n      className=\"-m-1 w-[calc(100%+--spacing(2))] bg-transparent\"\n      // cmdk owns the keyboard while focus is in the list; only Escape\n      // (close the menu) and Tab continue outward.\n      onKeyDown={(event) => {\n        if (event.key !== \"Escape\" && event.key !== \"Tab\") {\n          event.stopPropagation()\n        }\n      }}\n    >\n      <CommandInput\n        ref={inputRef}\n        placeholder=\"Search file types…\"\n        className=\"h-9\"\n      />\n      <CommandList className=\"max-h-none\">\n        <CommandEmpty>No file types found.</CommandEmpty>\n        <InlineScrollArea2 orientation=\"vertical\" className=\"h-auto max-h-64\">\n          {FILE_TYPE_FILTER_GROUPS.map((group) => {\n            const groupOptions = options.filter(\n              (option) => option.group === group\n            )\n            if (groupOptions.length === 0) return null\n            return (\n              <CommandGroup key={group} heading={group}>\n                {groupOptions.map((option) => {\n                  const isChecked = checkedMimes.includes(option.mime)\n                  return (\n                    <CommandItem\n                      key={option.mime}\n                      value={option.label}\n                      keywords={[option.mime]}\n                      onSelect={() => onToggle(option.mime, !isChecked)}\n                    >\n                      <IconPlaceholder\n                        lucide=\"Check\"\n                        tabler=\"IconCheck\"\n                        hugeicons=\"Tick02Icon\"\n                        phosphor=\"CheckIcon\"\n                        remixicon=\"RiCheckLine\"\n                        className={cn(\n                          \"size-4 text-foreground\",\n                          !isChecked && \"opacity-0\"\n                        )}\n                      />\n                      <FileTypeIcon\n                        fileName={option.iconFileName}\n                        className=\"size-4\"\n                      />\n                      {option.label}\n                    </CommandItem>\n                  )\n                })}\n              </CommandGroup>\n            )\n          })}\n        </InlineScrollArea2>\n      </CommandList>\n    </Command>\n  )\n}\n// Toolbar filter menu: file types as a searchable checklist, dates as\n// single-select presets plus a custom range, mirroring Extend's table\n// filters.\nfunction FileSystemFilterMenu({\n  fileTypeOptions,\n  filters,\n  onOpenCustomRange,\n  onSelectDatePreset,\n  onToggleFileType,\n}: {\n  fileTypeOptions: FileTypeFilterOption[]\n  filters: FileSystemFilter[]\n  onOpenCustomRange: (type: FileSystemDateFilterType) => void\n  onSelectDatePreset: (type: FileSystemDateFilterType, preset: string) => void\n  onToggleFileType: (mime: string, checked: boolean) => void\n}) {\n  const fileTypeFilter = filters.find((filter) => filter.type === \"fileType\")\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        render={\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"icon-sm\"\n            aria-label=\"Filter\"\n            title=\"Filter\"\n            className=\"relative size-7 sm:size-7\"\n          >\n            <IconPlaceholder\n              lucide=\"Filter\"\n              tabler=\"IconFilter\"\n              hugeicons=\"FilterIcon\"\n              phosphor=\"FunnelIcon\"\n              remixicon=\"RiFilterLine\"\n              className=\"size-4\"\n            />\n            {filters.length > 0 ? (\n              <span className=\"absolute top-1 right-1 size-1.5 rounded-full bg-primary\" />\n            ) : null}\n          </Button>\n        }\n      ></DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"min-w-44\">\n        <DropdownMenuSub>\n          <DropdownMenuSubTrigger>\n            <IconPlaceholder\n              lucide=\"FileArchiveIcon\"\n              tabler=\"IconFileZip\"\n              hugeicons=\"File01Icon\"\n              phosphor=\"FileZipIcon\"\n              remixicon=\"RiFileZipLine\"\n              className=\"size-4 text-muted-foreground\"\n            />\n            File type\n          </DropdownMenuSubTrigger>\n          <DropdownMenuSubContent className=\"w-60\">\n            <FileSystemFileTypeCommand\n              checkedMimes={fileTypeFilter?.value ?? []}\n              onToggle={onToggleFileType}\n              options={fileTypeOptions}\n            />\n          </DropdownMenuSubContent>\n        </DropdownMenuSub>\n        {([\"dateModified\", \"dateCreated\"] as const).map((type) => (\n          <DropdownMenuSub key={type}>\n            <DropdownMenuSubTrigger>\n              <IconPlaceholder\n                lucide=\"Calendar\"\n                tabler=\"IconCalendar\"\n                hugeicons=\"Calendar03Icon\"\n                phosphor=\"CalendarIcon\"\n                remixicon=\"RiCalendarLine\"\n                className=\"size-4 text-muted-foreground\"\n              />\n              {FILTER_TYPE_LABELS[type]}\n            </DropdownMenuSubTrigger>\n            <DropdownMenuSubContent>\n              <InlineScrollArea2\n                orientation=\"vertical\"\n                className=\"h-auto max-h-72\"\n              >\n                {DATE_FILTER_PRESETS.map((preset) => (\n                  <DropdownMenuItem\n                    key={preset}\n                    onClick={() => onSelectDatePreset(type, preset)}\n                  >\n                    {preset}\n                  </DropdownMenuItem>\n                ))}\n                <DropdownMenuItem onClick={() => onOpenCustomRange(type)}>\n                  Custom date range…\n                </DropdownMenuItem>\n              </InlineScrollArea2>\n            </DropdownMenuSubContent>\n          </DropdownMenuSub>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\nconst FILTER_PILL_SEGMENT_CLASSNAME =\n  \"flex h-5 items-center gap-1 border border-l-0 bg-background px-1.5 whitespace-nowrap text-foreground\"\nconst FILTER_PILL_BUTTON_CLASSNAME = cn(\n  FILTER_PILL_SEGMENT_CLASSNAME,\n  \"transition-colors outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring\"\n)\n// One applied filter, rendered as a segmented pill in the status bar:\n// type · operator · value · remove, each segment interactive like Extend's\n// table filter pills.\nfunction FileSystemFilterPill({\n  fileTypeOptions,\n  filter,\n  onOpenCustomRange,\n  onOperatorChange,\n  onRemove,\n  onSelectDatePreset,\n  onToggleFileType,\n}: {\n  fileTypeOptions: FileTypeFilterOption[]\n  filter: FileSystemFilter\n  onOpenCustomRange?: () => void\n  onOperatorChange: (operator: FileSystemFilterOperator) => void\n  onRemove: () => void\n  onSelectDatePreset: (preset: string) => void\n  onToggleFileType: (mime: string, checked: boolean) => void\n}) {\n  const isCustomRange =\n    filter.type !== \"fileType\" && isCustomDateRangeValue(filter.value)\n  const selectedTypeLabels =\n    filter.type === \"fileType\"\n      ? filter.value.map(\n          (mime) =>\n            fileTypeOptions.find((option) => option.mime === mime)?.label ??\n            mime\n        )\n      : []\n  return (\n    <div className=\"flex items-center text-xs\">\n      <span\n        className={cn(\n          FILTER_PILL_SEGMENT_CLASSNAME,\n          \"rounded-l-md border-l text-primary\"\n        )}\n      >\n        {filter.type === \"fileType\" ? (\n          <File01Glyph className=\"size-3\" />\n        ) : (\n          <Calendar03Glyph className=\"size-3\" />\n        )}\n        {FILTER_TYPE_LABELS[filter.type]}\n      </span>\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          render={\n            <button\n              type=\"button\"\n              className={cn(FILTER_PILL_BUTTON_CLASSNAME, \"text-primary\")}\n            >\n              {FILTER_OPERATOR_LABELS[filter.operator]}\n            </button>\n          }\n        ></DropdownMenuTrigger>\n        <DropdownMenuContent align=\"start\" className=\"min-w-28\">\n          {filterOperatorChoices(filter).map((operator) => (\n            <DropdownMenuItem\n              key={operator}\n              onClick={() => onOperatorChange(operator)}\n            >\n              {FILTER_OPERATOR_LABELS[operator]}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenu>\n      {filter.type === \"fileType\" ? (\n        <DropdownMenu>\n          <DropdownMenuTrigger\n            render={\n              <button\n                type=\"button\"\n                title={selectedTypeLabels.join(\", \")}\n                className={FILTER_PILL_BUTTON_CLASSNAME}\n              >\n                {filter.value.length === 1\n                  ? selectedTypeLabels[0]\n                  : `${filter.value.length} selected`}\n              </button>\n            }\n          ></DropdownMenuTrigger>\n          <DropdownMenuContent align=\"start\" className=\"w-60\">\n            <FileSystemFileTypeCommand\n              checkedMimes={filter.value}\n              onToggle={onToggleFileType}\n              options={fileTypeOptions}\n            />\n          </DropdownMenuContent>\n        </DropdownMenu>\n      ) : isCustomRange ? (\n        <button\n          type=\"button\"\n          onClick={onOpenCustomRange}\n          className={FILTER_PILL_BUTTON_CLASSNAME}\n        >\n          {filter.value\n            .map((value) => new Date(value).toLocaleDateString(\"en-US\"))\n            .join(\" – \")}\n        </button>\n      ) : (\n        <DropdownMenu>\n          <DropdownMenuTrigger\n            render={\n              <button type=\"button\" className={FILTER_PILL_BUTTON_CLASSNAME}>\n                {filter.value[0]}\n              </button>\n            }\n          ></DropdownMenuTrigger>\n          <DropdownMenuContent align=\"start\">\n            <InlineScrollArea2\n              orientation=\"vertical\"\n              className=\"h-auto max-h-72\"\n            >\n              {DATE_FILTER_PRESETS.map((preset) => (\n                <DropdownMenuItem\n                  key={preset}\n                  onClick={() => onSelectDatePreset(preset)}\n                >\n                  {preset}\n                </DropdownMenuItem>\n              ))}\n              <DropdownMenuItem onClick={onOpenCustomRange}>\n                Custom date range…\n              </DropdownMenuItem>\n            </InlineScrollArea2>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      )}\n      <button\n        type=\"button\"\n        aria-label={`Remove ${FILTER_TYPE_LABELS[filter.type]} filter`}\n        onClick={onRemove}\n        className={cn(\n          FILTER_PILL_BUTTON_CLASSNAME,\n          \"rounded-r-md px-1 text-muted-foreground hover:text-foreground\"\n        )}\n      >\n        <IconPlaceholder\n          lucide=\"X\"\n          tabler=\"IconX\"\n          hugeicons=\"Cancel01Icon\"\n          phosphor=\"XIcon\"\n          remixicon=\"RiCloseLine\"\n          className=\"size-3\"\n        />\n      </button>\n    </div>\n  )\n}\nfunction formatDateInputValue(date: Date | undefined) {\n  if (!date) return \"\"\n  const pad = (value: number) => String(value).padStart(2, \"0\")\n  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`\n}\nfunction parseDateInputValue(value: string) {\n  const trimmed = value.trim()\n  if (!trimmed) return undefined\n  const isoMatch = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/.exec(trimmed)\n  if (isoMatch) {\n    const date = new Date(\n      Number(isoMatch[1]),\n      Number(isoMatch[2]) - 1,\n      Number(isoMatch[3])\n    )\n    return Number.isNaN(date.getTime()) ? undefined : date\n  }\n  const parsed = Date.parse(trimmed)\n  return Number.isNaN(parsed) ? undefined : new Date(parsed)\n}\nconst DATE_RANGE_DIALOG_PRESETS = [\n  \"Last 7 days\",\n  \"This month\",\n  \"Last 1 month\",\n  \"Last 3 months\",\n  \"This year\",\n  \"Last 12 months\",\n]\nfunction dateRangePresetRange(preset: string) {\n  const from = new Date()\n  const to = new Date()\n  from.setHours(0, 0, 0, 0)\n  to.setHours(23, 59, 59, 999)\n  switch (preset) {\n    case \"Last 7 days\":\n      from.setDate(from.getDate() - 6)\n      break\n    case \"This month\":\n      from.setDate(1)\n      break\n    case \"Last 1 month\":\n      from.setMonth(from.getMonth() - 1)\n      break\n    case \"Last 3 months\":\n      from.setMonth(from.getMonth() - 3)\n      break\n    case \"This year\":\n      from.setMonth(0, 1)\n      break\n    case \"Last 12 months\":\n      from.setFullYear(from.getFullYear() - 1)\n      break\n  }\n  return { from, to }\n}\nconst WEEKDAY_LABELS = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"]\nfunction calendarDayKey(date: Date) {\n  return date.getFullYear() * 10000 + date.getMonth() * 100 + date.getDate()\n}\n// Two-month range calendar for the custom date range dialog (one month at\n// phone widths). Clicking sets the start, then the end; clicking before the\n// start swaps the ends, and a third click restarts the range.\nfunction FileSystemRangeCalendar({\n  onSelect,\n  range,\n}: {\n  onSelect: (range: { from?: Date; to?: Date }) => void\n  range: {\n    from?: Date\n    to?: Date\n  }\n}) {\n  const [viewMonth, setViewMonth] = React.useState(() => {\n    const base = range.from ?? new Date()\n    return new Date(base.getFullYear(), base.getMonth(), 1)\n  })\n  const months = [\n    viewMonth,\n    new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1),\n  ]\n  const fromKey = range.from ? calendarDayKey(range.from) : null\n  const toKey = range.to ? calendarDayKey(range.to) : null\n  const todayKey = calendarDayKey(new Date())\n  const handleDayClick = (day: Date) => {\n    if (!range.from || range.to) {\n      onSelect({ from: day })\n    } else if (calendarDayKey(day) < calendarDayKey(range.from)) {\n      onSelect({ from: day, to: range.from })\n    } else {\n      onSelect({ from: range.from, to: day })\n    }\n  }\n  return (\n    <div className=\"relative\">\n      <button\n        type=\"button\"\n        aria-label=\"Previous month\"\n        onClick={() =>\n          setViewMonth(\n            (previous) =>\n              new Date(previous.getFullYear(), previous.getMonth() - 1, 1)\n          )\n        }\n        className=\"absolute top-0 left-0 flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <IconPlaceholder\n          lucide=\"ChevronLeft\"\n          tabler=\"IconChevronLeft\"\n          hugeicons=\"ArrowLeft01Icon\"\n          phosphor=\"CaretLeftIcon\"\n          remixicon=\"RiArrowLeftSLine\"\n          className=\"size-4\"\n        />\n      </button>\n      <button\n        type=\"button\"\n        aria-label=\"Next month\"\n        onClick={() =>\n          setViewMonth(\n            (previous) =>\n              new Date(previous.getFullYear(), previous.getMonth() + 1, 1)\n          )\n        }\n        className=\"absolute top-0 right-0 flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <IconPlaceholder\n          lucide=\"ArrowRight\"\n          tabler=\"IconArrowRight\"\n          hugeicons=\"ArrowRight01Icon\"\n          phosphor=\"ArrowRightIcon\"\n          remixicon=\"RiArrowRightLine\"\n          className=\"size-4\"\n        />\n      </button>\n      <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n        {months.map((month, monthIndex) => {\n          const firstWeekday = month.getDay()\n          const dayCount = new Date(\n            month.getFullYear(),\n            month.getMonth() + 1,\n            0\n          ).getDate()\n          const cells = [\n            ...Array.from({ length: firstWeekday }, () => null),\n            ...Array.from(\n              { length: dayCount },\n              (_, index) =>\n                new Date(month.getFullYear(), month.getMonth(), index + 1)\n            ),\n          ]\n          return (\n            <div\n              key={`${month.getFullYear()}-${month.getMonth()}`}\n              className={cn(monthIndex === 1 && \"max-sm:hidden\")}\n            >\n              <div className=\"text-center text-sm leading-6 font-medium\">\n                {month.toLocaleDateString(\"en-US\", {\n                  month: \"long\",\n                  year: \"numeric\",\n                })}\n              </div>\n              <div className=\"mt-1 grid grid-cols-7 text-center text-xs text-muted-foreground\">\n                {WEEKDAY_LABELS.map((weekday) => (\n                  <span key={weekday} className=\"h-6 leading-6\">\n                    {weekday}\n                  </span>\n                ))}\n              </div>\n              <div className=\"grid grid-cols-7 gap-y-px\">\n                {cells.map((day, cellIndex) => {\n                  if (!day) return <span key={cellIndex} />\n                  const dayKey = calendarDayKey(day)\n                  const isFrom = dayKey === fromKey\n                  const isTo = dayKey === toKey\n                  const isWithinRange =\n                    fromKey !== null &&\n                    toKey !== null &&\n                    dayKey > fromKey &&\n                    dayKey < toKey\n                  return (\n                    <button\n                      key={cellIndex}\n                      type=\"button\"\n                      onClick={() => handleDayClick(day)}\n                      className={cn(\n                        \"flex h-7 items-center justify-center rounded-md text-xs tabular-nums transition-colors outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring\",\n                        isWithinRange && \"rounded-none bg-accent\",\n                        (isFrom || isTo) &&\n                          \"bg-primary text-primary-foreground hover:bg-primary\",\n                        isFrom &&\n                          toKey !== null &&\n                          fromKey !== toKey &&\n                          \"rounded-r-none\",\n                        isTo && fromKey !== toKey && \"rounded-l-none\",\n                        dayKey === todayKey &&\n                          !isFrom &&\n                          !isTo &&\n                          \"font-semibold text-primary\"\n                      )}\n                    >\n                      {day.getDate()}\n                    </button>\n                  )\n                })}\n              </div>\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n// Custom date range dialog mirroring Extend's table filters: From/To inputs,\n// a two-month range calendar, and quick presets. Applied ranges span from\n// the start of the first day to the end of the last.\nfunction FileSystemDateRangeDialog({\n  initialRange,\n  onApply,\n  onClose,\n}: {\n  initialRange?: {\n    from: Date\n    to: Date\n  }\n  onApply: (from: Date, to: Date) => void\n  onClose: () => void\n}) {\n  const [range, setRange] = React.useState<{\n    from?: Date\n    to?: Date\n  }>(() => initialRange ?? {})\n  const [fromInput, setFromInput] = React.useState(() =>\n    formatDateInputValue(initialRange?.from)\n  )\n  const [toInput, setToInput] = React.useState(() =>\n    formatDateInputValue(initialRange?.to)\n  )\n  const selectRange = (next: { from?: Date; to?: Date }) => {\n    setRange(next)\n    if (next.from) setFromInput(formatDateInputValue(next.from))\n    if (next.to) setToInput(formatDateInputValue(next.to))\n  }\n  const dateField = (\n    label: string,\n    value: string,\n    onChange: (value: string) => void\n  ) => (\n    <div className=\"flex flex-1 flex-col gap-1.5\">\n      <span className=\"text-xs font-medium\">{label}</span>\n      <div className=\"relative flex items-center\">\n        <IconPlaceholder\n          lucide=\"Calendar\"\n          tabler=\"IconCalendar\"\n          hugeicons=\"Calendar03Icon\"\n          phosphor=\"CalendarIcon\"\n          remixicon=\"RiCalendarLine\"\n          className=\"pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground\"\n        />\n        <Input\n          type=\"text\"\n          value={value}\n          placeholder=\"YYYY-MM-DD\"\n          aria-label={`${label} date`}\n          onChange={(event) => onChange(event.target.value)}\n          className=\"h-8 pl-8 sm:h-8\"\n        />\n      </div>\n    </div>\n  )\n  return (\n    <Dialog\n      open\n      onOpenChange={(open) => {\n        if (!open) onClose()\n      }}\n    >\n      <DialogContent className=\"w-[30rem] max-w-[calc(100vw-2rem)]\">\n        <DialogHeader>\n          <DialogTitle>Custom date range</DialogTitle>\n        </DialogHeader>\n        <InlineDialogPanel className=\"flex flex-col gap-4\">\n          <div className=\"flex gap-3\">\n            {dateField(\"From\", fromInput, (value) => {\n              setFromInput(value)\n              const parsed = parseDateInputValue(value)\n              if (parsed)\n                setRange((previous) => ({ ...previous, from: parsed }))\n            })}\n            {dateField(\"To\", toInput, (value) => {\n              setToInput(value)\n              const parsed = parseDateInputValue(value)\n              if (parsed) setRange((previous) => ({ ...previous, to: parsed }))\n            })}\n          </div>\n          <FileSystemRangeCalendar range={range} onSelect={selectRange} />\n          <div className=\"grid grid-cols-3 gap-2\">\n            {DATE_RANGE_DIALOG_PRESETS.map((preset) => (\n              <Button\n                key={preset}\n                type=\"button\"\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={() => selectRange(dateRangePresetRange(preset))}\n              >\n                {preset}\n              </Button>\n            ))}\n          </div>\n        </InlineDialogPanel>\n        <DialogFooter>\n          <Button type=\"button\" variant=\"outline\" onClick={onClose}>\n            Cancel\n          </Button>\n          <Button\n            type=\"button\"\n            disabled={!range.from || !range.to}\n            onClick={() => {\n              if (!range.from || !range.to) return\n              const from = new Date(range.from)\n              const to = new Date(range.to)\n              from.setHours(0, 0, 0, 0)\n              to.setHours(23, 59, 59, 999)\n              onApply(from, to)\n            }}\n          >\n            Apply\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\ntype FileSystemViewProps = {\n  currentPath: string\n  entries: FileSystemEntry[]\n  fileFilter: ((file: FileEntry) => boolean) | null\n  getFileUrl?: (file: FileSystemFileItem) => string | Promise<string>\n  index: FileSystemIndex\n  loadPreviewImageUrl?: (\n    file: FileSystemFileItem,\n    pageIndex: number\n  ) => Promise<string | null>\n  loadingFolders: Set<string>\n  onOpen: (entry: FileSystemEntry) => void\n  onSelect: (entry: FileSystemEntry | null) => void\n  onSortColumnClick: (key: FileSystemSortKey) => void\n  /** Pooled paths currently attached to the DOM (reveal instantly). */\n  attachedStagePaths: string[]\n  /** `\"path#pageIndex\"` → thumbnail URL, shared by every pager. */\n  pageUrlCache: Map<string, string>\n  /** Admits a file into the root-owned keep-alive preview pool. */\n  poolStagePath: (path: string) => void\n  /** Mounts/unmounts the gallery host element for a pooled path. */\n  registerStageHost: (path: string, element: HTMLElement | null) => void\n  renderFilePreview?: (file: FileSystemFileItem) => React.ReactNode\n  searchQuery: string\n  selectedEntry: FileSystemEntry | null\n  selectedPath: string | null\n  sort: FileSystemSortState\n  /** Expanded tree folders per folder path, surviving view switches. */\n  treeExpansionRef: React.RefObject<Map<string, readonly string[]>>\n}\n// Resolves a display URL for a file: its own `url`, else via `getFileUrl`.\n// Keyed by path/url (not object identity) so manifest churn — e.g. thumbnails\n// streaming in — doesn't re-trigger presign calls for the same file. An\n// optional `cache` shared across mounts serves revisited files synchronously:\n// no repeat presign round-trip (and no loading flash), and the stable URL\n// keeps the browser's HTTP cache valid for already-fetched content.\nfunction useResolvedFileUrl(\n  file: FileEntry | null,\n  getFileUrl?: (file: FileSystemFileItem) => string | Promise<string>,\n  cache?: Map<string, string>\n) {\n  const [state, setState] = React.useState<{\n    isResolving: boolean\n    url: string | null\n  }>(() => ({\n    isResolving: false,\n    url: file ? (file.url ?? cache?.get(file.path) ?? null) : null,\n  }))\n  const fileRef = React.useRef(file)\n  React.useEffect(() => {\n    fileRef.current = file\n  })\n  const filePath = file?.path ?? null\n  const fileUrl = file?.url ?? null\n  React.useEffect(() => {\n    const currentFile = fileRef.current\n    const knownUrl =\n      fileUrl ?? (filePath ? (cache?.get(filePath) ?? null) : null)\n    if (!currentFile || knownUrl || !getFileUrl) {\n      setState({ isResolving: false, url: knownUrl })\n      return\n    }\n    let isCurrent = true\n    setState({ isResolving: true, url: null })\n    void Promise.resolve(getFileUrl(currentFile))\n      .then((url) => {\n        if (url) cache?.set(currentFile.path, url)\n        if (isCurrent) setState({ isResolving: false, url })\n      })\n      .catch(() => {\n        if (isCurrent) setState({ isResolving: false, url: null })\n      })\n    return () => {\n      isCurrent = false\n    }\n  }, [cache, filePath, fileUrl, getFileUrl])\n  return state\n}\n// Returns `value` once it has stopped changing for `delay` ms. Gallery\n// navigation scrubs past files quickly; heavy previews (document viewers,\n// presigned URL resolution) only kick in for the file the user lands on.\nfunction useSettledValue<T>(value: T, delay: number): T {\n  const [settled, setSettled] = React.useState(value)\n  React.useEffect(() => {\n    if (Object.is(settled, value)) return\n    const timeout = window.setTimeout(() => setSettled(value), delay)\n    return () => window.clearTimeout(timeout)\n  }, [delay, settled, value])\n  return settled\n}\nfunction FileSystemEmptyState({\n  label,\n  isLoading = false,\n}: {\n  label: string\n  isLoading?: boolean\n}) {\n  return (\n    <div\n      className={cn(\n        \"flex size-full items-center justify-center text-sm text-muted-foreground\",\n        isLoading && \"animate-pulse motion-reduce:animate-none\"\n      )}\n    >\n      {label}\n    </div>\n  )\n}\nconst ARROW_KEYS = new Set([\"ArrowDown\", \"ArrowLeft\", \"ArrowRight\", \"ArrowUp\"])\n// Type-ahead buffers reset after this idle period, like Finder.\nconst TYPE_AHEAD_RESET_MS = 700\n// Letters and digits only — the same key test the tree uses — so shortcuts\n// and whitespace scrolling stay untouched.\nfunction isTypeAheadKey(event: React.KeyboardEvent) {\n  return (\n    event.key.length === 1 &&\n    /^[\\p{L}\\p{N}]$/u.test(event.key) &&\n    !event.altKey &&\n    !event.ctrlKey &&\n    !event.metaKey\n  )\n}\n// Shared Finder-style type-ahead used by every view: printable keys\n// accumulate a buffer that jumps to the next entry whose name starts with\n// it, and repeating a single letter cycles through entries with that\n// prefix. Each view passes its own display-ordered candidate list, so the\n// same keystrokes land on the same file everywhere.\nfunction useEntryTypeAhead() {\n  const stateRef = React.useRef({ buffer: \"\", timeout: 0 })\n  React.useEffect(() => {\n    const state = stateRef.current\n    return () => window.clearTimeout(state.timeout)\n  }, [])\n  return React.useCallback(\n    (\n      event: React.KeyboardEvent,\n      entries: readonly FileSystemEntry[],\n      currentIndex: number\n    ) => {\n      if (!isTypeAheadKey(event) || entries.length === 0) return null\n      // Embedded viewers (and any future inputs) keep their keystrokes.\n      const target = event.target\n      if (\n        target instanceof HTMLElement &&\n        (target.isContentEditable ||\n          target.tagName === \"INPUT\" ||\n          target.tagName === \"TEXTAREA\" ||\n          target.tagName === \"SELECT\")\n      ) {\n        return null\n      }\n      const state = stateRef.current\n      window.clearTimeout(state.timeout)\n      state.timeout = window.setTimeout(() => {\n        state.buffer = \"\"\n      }, TYPE_AHEAD_RESET_MS)\n      state.buffer += event.key.toLowerCase()\n      // A repeated single letter advances past the current entry; a longer\n      // buffer refines the match in place.\n      const startIndex =\n        currentIndex < 0\n          ? 0\n          : currentIndex + (state.buffer.length === 1 ? 1 : 0)\n      for (let step = 0; step < entries.length; step += 1) {\n        const entry = entries[(startIndex + step) % entries.length]\n        if (entry.name.toLowerCase().startsWith(state.buffer)) {\n          event.preventDefault()\n          return entry\n        }\n      }\n      event.preventDefault()\n      return null\n    },\n    []\n  )\n}\n// Selects (and focuses) the entry reached by an arrow key. Up/down use row\n// geometry so navigation follows the rendered auto-fill grid.\nfunction moveGridSelection({\n  entries,\n  itemRefs,\n  key,\n  onSelect,\n  selectedPath,\n}: {\n  entries: FileSystemEntry[]\n  itemRefs: Map<string, HTMLButtonElement>\n  key: string\n  onSelect: (entry: FileSystemEntry | null) => void\n  selectedPath: string | null\n}) {\n  if (entries.length === 0) return false\n  const currentIndex = entries.findIndex((entry) => entry.path === selectedPath)\n  let nextEntry: FileSystemEntry | undefined\n  if (currentIndex === -1) {\n    nextEntry = entries[0]\n  } else if (key === \"ArrowLeft\" || key === \"ArrowRight\") {\n    nextEntry = entries[currentIndex + (key === \"ArrowLeft\" ? -1 : 1)]\n  } else {\n    const currentElement = itemRefs.get(entries[currentIndex].path)\n    if (!currentElement) return false\n    const currentRect = currentElement.getBoundingClientRect()\n    let bestScore = Infinity\n    for (const entry of entries) {\n      if (entry.path === selectedPath) continue\n      const rect = itemRefs.get(entry.path)?.getBoundingClientRect()\n      if (!rect) continue\n      const rowDelta =\n        key === \"ArrowDown\"\n          ? rect.top - currentRect.top\n          : currentRect.top - rect.top\n      if (rowDelta <= 1) continue\n      const score = rowDelta * 1000 + Math.abs(rect.left - currentRect.left)\n      if (score < bestScore) {\n        bestScore = score\n        nextEntry = entry\n      }\n    }\n  }\n  if (!nextEntry) return false\n  onSelect(nextEntry)\n  itemRefs.get(nextEntry.path)?.focus()\n  return true\n}\n// Icon grid geometry (px at the default 16px root font size). Tiles have a\n// fixed height — a 4rem glyph box plus a reserved two-line label — so rows\n// share one stride and the grid can window cleanly.\nconst ICON_GRID_PADDING = 12 // p-3\nconst ICON_MIN_TILE_WIDTH = 104 // 6.5rem\nconst ICON_TILE_GAP_X = 4 // gap-x-1\nconst ICON_TILE_HEIGHT = 102 // h-16 glyph box + gap-1.5 + two text-xs lines\nconst ICON_ROW_GAP = 12 // gap-y-3\nconst ICON_ROW_STRIDE = ICON_TILE_HEIGHT + ICON_ROW_GAP\nfunction FileSystemIconsView({\n  entries,\n  onOpen,\n  onSelect,\n  renderFilePreview,\n  selectedPath,\n}: FileSystemViewProps) {\n  const itemRefs = React.useRef(new Map<string, HTMLButtonElement>())\n  const viewportRef = React.useRef<HTMLDivElement | null>(null)\n  const typeAhead = useEntryTypeAhead()\n  // The column count mirrors what `repeat(auto-fill, minmax(6.5rem, 1fr))`\n  // produces (the CSS owns the actual layout) so item indices map to grid\n  // rows — the windowing below depends on that mapping. It stays null until\n  // the first client measure; server markup must not guess.\n  const [columnCount, setColumnCount] = React.useState<number | null>(null)\n  React.useLayoutEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || typeof ResizeObserver === \"undefined\") return\n    const update = () => {\n      const available = viewport.clientWidth - ICON_GRID_PADDING * 2\n      setColumnCount(\n        Math.max(\n          1,\n          Math.floor(\n            (available + ICON_TILE_GAP_X) /\n              (ICON_MIN_TILE_WIDTH + ICON_TILE_GAP_X)\n          )\n        )\n      )\n    }\n    const observer = new ResizeObserver(update)\n    update()\n    observer.observe(viewport)\n    return () => observer.disconnect()\n  }, [])\n  const resolvedColumnCount = columnCount ?? 1\n  const rowCount = Math.ceil(entries.length / resolvedColumnCount)\n  const { end, start } = useVirtualWindow({\n    count: rowCount,\n    itemStride: ICON_ROW_STRIDE,\n    leadingPx: ICON_GRID_PADDING,\n    overscan: 4,\n    viewportRef,\n  })\n  const visibleEntries = entries.slice(\n    start * resolvedColumnCount,\n    end * resolvedColumnCount\n  )\n  // Selection can land outside the window (view switches, shrinking results);\n  // bring its row back into the viewport so the tile mounts and is focusable.\n  React.useLayoutEffect(() => {\n    if (!selectedPath) return\n    const entryIndex = entries.findIndex((entry) => entry.path === selectedPath)\n    if (entryIndex === -1) return\n    scrollIndexIntoView({\n      index: Math.floor(entryIndex / resolvedColumnCount),\n      itemSize: ICON_TILE_HEIGHT,\n      itemStride: ICON_ROW_STRIDE,\n      leadingPx: ICON_GRID_PADDING,\n      viewport: viewportRef.current,\n    })\n  }, [entries, resolvedColumnCount, selectedPath])\n  // Roving tabindex: the grid is a single tab stop (the selected tile when\n  // rendered, else the first rendered one), so Shift+Tab returns to the\n  // toolbar like in the list view.\n  const tabStopPath = visibleEntries.some(\n    (entry) => entry.path === selectedPath\n  )\n    ? selectedPath\n    : (visibleEntries[0]?.path ?? null)\n  return (\n    <InlineScrollArea2\n      orientation=\"vertical\"\n      viewportRef={viewportRef}\n      viewportClassName=\"p-3\"\n      viewportProps={{\n        onClick: (event) => {\n          if (event.target === event.currentTarget) onSelect(null)\n        },\n      }}\n    >\n      <div\n        className=\"relative\"\n        // The scroll-height spacer needs the measured column count; until\n        // then the absolutely positioned grid alone defines the scrollable\n        // overflow, so the server-rendered frame doesn't flash an\n        // oversized scroll range.\n        style={{\n          height:\n            columnCount !== null && rowCount\n              ? rowCount * ICON_ROW_STRIDE - ICON_ROW_GAP\n              : undefined,\n        }}\n      >\n        <div\n          role=\"listbox\"\n          aria-label=\"Files\"\n          className=\"absolute inset-x-0 grid gap-x-1 gap-y-3\"\n          // The auto-fill expression produces the same column count the\n          // ResizeObserver measures (the measurement exists only for the\n          // windowing math), so the server-rendered first paint is already\n          // a grid instead of flashing a single stacked column until the\n          // first client measure.\n          style={{\n            gridTemplateColumns: \"repeat(auto-fill, minmax(6.5rem, 1fr))\",\n            top: start * ICON_ROW_STRIDE,\n          }}\n          onKeyDown={(event) => {\n            if (!ARROW_KEYS.has(event.key)) {\n              const match = typeAhead(\n                event,\n                entries,\n                entries.findIndex((entry) => entry.path === selectedPath)\n              )\n              if (match) {\n                onSelect(match)\n                // The matched tile may be outside the virtual window; the\n                // selection effect scrolls it in, and focus follows once it\n                // has mounted.\n                requestAnimationFrame(() =>\n                  itemRefs.current.get(match.path)?.focus()\n                )\n              }\n              return\n            }\n            if (\n              moveGridSelection({\n                entries,\n                itemRefs: itemRefs.current,\n                key: event.key,\n                onSelect,\n                selectedPath,\n              })\n            ) {\n              event.preventDefault()\n            }\n          }}\n        >\n          {visibleEntries.map((entry) => {\n            const isSelected = entry.path === selectedPath\n            return (\n              <button\n                key={entry.path}\n                type=\"button\"\n                role=\"option\"\n                aria-selected={isSelected}\n                tabIndex={entry.path === tabStopPath ? 0 : -1}\n                ref={(element) => {\n                  if (element) {\n                    itemRefs.current.set(entry.path, element)\n                  } else {\n                    itemRefs.current.delete(entry.path)\n                  }\n                }}\n                onClick={() => onSelect(entry)}\n                onDoubleClick={() => onOpen(entry)}\n                onKeyDown={(event) => {\n                  if (event.key === \"Enter\") onOpen(entry)\n                }}\n                className=\"group flex h-[6.375rem] flex-col items-center gap-1.5 outline-none\"\n              >\n                <span\n                  className={cn(\n                    \"flex h-16 w-20 shrink-0 items-center justify-center rounded-lg p-1 transition-colors group-focus-visible:ring-2 group-focus-visible:ring-ring\",\n                    isSelected && \"bg-accent\"\n                  )}\n                >\n                  {entry.kind === \"folder\" ? (\n                    <FileSystemFolderGlyph className=\"h-13 w-auto drop-shadow-sm\" />\n                  ) : (\n                    <FileVisual\n                      file={entry}\n                      className={cn(\n                        \"rounded-sm shadow-xs\",\n                        // Landscape thumbnails get extra width so they fill\n                        // the tile instead of rendering as a short sliver.\n                        (entry.previewAspectRatio ?? 0.78) > 1.2\n                          ? \"w-[4.75rem]\"\n                          : \"w-12\"\n                      )}\n                      previewAspectRatio={0.78}\n                      renderFilePreview={renderFilePreview}\n                    />\n                  )}\n                </span>\n                <span\n                  className={cn(\n                    \"max-w-full rounded-sm px-1.5 py-px text-center text-xs leading-tight break-words\",\n                    isSelected\n                      ? \"bg-primary text-primary-foreground\"\n                      : \"text-foreground\"\n                  )}\n                >\n                  <span className=\"line-clamp-2\">{entry.name}</span>\n                </span>\n              </button>\n            )\n          })}\n        </div>\n      </div>\n    </InlineScrollArea2>\n  )\n}\n// One sortable column header for the list view; the active column shows the\n// direction chevron on its right.\nfunction FileSystemListColumnHeader({\n  className,\n  label,\n  onClick,\n  sort,\n  sortKey,\n}: {\n  className?: string\n  label: string\n  onClick: (key: FileSystemSortKey) => void\n  sort: FileSystemSortState\n  sortKey: FileSystemSortKey\n}) {\n  const isActive = sort.key === sortKey\n  return (\n    <button\n      type=\"button\"\n      onClick={() => onClick(sortKey)}\n      className={cn(\n        \"flex items-center gap-0.5 rounded-sm py-0.5 transition-colors outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n        isActive && \"text-foreground\",\n        className\n      )}\n    >\n      {label}\n      {isActive ? (\n        sort.direction === \"asc\" ? (\n          <ArrowUp01Glyph className=\"size-3 shrink-0\" />\n        ) : (\n          <ArrowDown01Glyph className=\"size-3 shrink-0\" />\n        )\n      ) : null}\n    </button>\n  )\n}\nfunction FileSystemListView({\n  currentPath,\n  fileFilter,\n  index,\n  onOpen,\n  onSelect,\n  onSortColumnClick,\n  searchQuery,\n  selectedPath,\n  sort,\n  treeExpansionRef,\n}: FileSystemViewProps) {\n  // Filters narrow the path list handed to the tree; the search query stays\n  // out of it so the tree's own search session (with match highlighting)\n  // keeps handling it without remounts per keystroke.\n  const relativePaths = React.useMemo(() => {\n    const paths: string[] = []\n    for (const [path, file] of index.files) {\n      if (currentPath === \"\" || path.startsWith(currentPath)) {\n        const relativePath = path.slice(currentPath.length)\n        if (!relativePath) continue\n        if (fileFilter && !fileFilter(file)) continue\n        paths.push(relativePath)\n      }\n    }\n    return paths.sort()\n  }, [currentPath, fileFilter, index])\n  if (relativePaths.length === 0) {\n    return (\n      <FileSystemEmptyState\n        label={\n          fileFilter\n            ? \"No items match the active filters\"\n            : \"This folder is empty\"\n        }\n      />\n    )\n  }\n  return (\n    <div className=\"flex size-full flex-col\">\n      {/* Paddings match the tree's row geometry: name text starts 46px in\n            (16px tree padding + 30px icon lane), metadata ends 24px from the\n            right (16px tree padding + 8px decoration inset). */}\n      <div className=\"flex shrink-0 items-center border-b py-1 pr-6 pl-[46px] text-xs font-medium text-muted-foreground\">\n        <FileSystemListColumnHeader\n          className=\"flex-1 justify-start\"\n          label=\"Name\"\n          onClick={onSortColumnClick}\n          sort={sort}\n          sortKey=\"name\"\n        />\n        <FileSystemListColumnHeader\n          className=\"w-44 justify-start\"\n          label=\"Date Modified\"\n          onClick={onSortColumnClick}\n          sort={sort}\n          sortKey=\"updatedAt\"\n        />\n        <FileSystemListColumnHeader\n          className=\"w-20 justify-start\"\n          label=\"Size\"\n          onClick={onSortColumnClick}\n          sort={sort}\n          sortKey=\"size\"\n        />\n      </div>\n      {/* Keyed by folder only: navigation remounts the tree, while filter,\n            sort, and manifest changes update the mounted model in place so\n            folder disclosure state survives them. */}\n      <FileSystemPierreTree\n        key={currentPath}\n        currentPath={currentPath}\n        hasActiveFilters={fileFilter !== null}\n        index={index}\n        initialSelectedPath={\n          selectedPath?.startsWith(currentPath)\n            ? selectedPath.slice(currentPath.length).replace(/\\/$/, \"\")\n            : null\n        }\n        onOpen={onOpen}\n        onSelect={onSelect}\n        relativePaths={relativePaths}\n        searchQuery={searchQuery}\n        sort={sort}\n        treeExpansionRef={treeExpansionRef}\n      />\n    </div>\n  )\n}\n// Embedded thumbnail symbols grow the sprite injected into the tree's shadow\n// DOM (data-URL covers can run hundreds of KB each); past this many the\n// remaining files fall back to the built-in file-type icons alone.\nconst TREE_THUMBNAIL_SPRITE_LIMIT = 400\nfunction FileSystemPierreTree({\n  currentPath,\n  hasActiveFilters,\n  index,\n  initialSelectedPath,\n  onOpen,\n  onSelect,\n  relativePaths,\n  searchQuery,\n  sort,\n  treeExpansionRef,\n}: {\n  currentPath: string\n  hasActiveFilters: boolean\n  index: FileSystemIndex\n  initialSelectedPath: string | null\n  onOpen: (entry: FileSystemEntry) => void\n  onSelect: (entry: FileSystemEntry | null) => void\n  relativePaths: string[]\n  searchQuery: string\n  sort: FileSystemSortState\n  treeExpansionRef: React.RefObject<Map<string, readonly string[]>>\n}) {\n  // The tree's comparator receives whole paths, not siblings, so it walks\n  // the shared segments and applies the active sort at the first level the\n  // two paths diverge — keeping directories first per level, the tree's\n  // default convention. Lookups go through the index maps, which are stable\n  // across search keystrokes.\n  const indexFiles = index.files\n  const indexFolders = index.folders\n  const sortComparator = React.useMemo<\n    \"default\" | FileTreeSortComparator\n  >(() => {\n    if (\n      sort.key === DEFAULT_SORT.key &&\n      sort.direction === DEFAULT_SORT.direction\n    ) {\n      return \"default\"\n    }\n    const entryAtDepth = (sortEntry: FileTreeSortEntry, depth: number) => {\n      const isDirectory =\n        depth < sortEntry.segments.length - 1 || sortEntry.isDirectory\n      const absolutePath = `${currentPath}${sortEntry.segments\n        .slice(0, depth + 1)\n        .join(\"/\")}${isDirectory ? \"/\" : \"\"}`\n      return isDirectory\n        ? indexFolders.get(absolutePath)\n        : indexFiles.get(absolutePath)\n    }\n    return (left, right) => {\n      const sharedDepth = Math.min(left.segments.length, right.segments.length)\n      for (let depth = 0; depth < sharedDepth; depth += 1) {\n        if (left.segments[depth] === right.segments[depth]) continue\n        const leftIsDirectory =\n          depth < left.segments.length - 1 || left.isDirectory\n        const rightIsDirectory =\n          depth < right.segments.length - 1 || right.isDirectory\n        if (leftIsDirectory !== rightIsDirectory) {\n          return leftIsDirectory ? -1 : 1\n        }\n        const leftEntry = entryAtDepth(left, depth)\n        const rightEntry = entryAtDepth(right, depth)\n        if (leftEntry && rightEntry) {\n          return compareEntriesBySort(leftEntry, rightEntry, sort)\n        }\n        return left.segments[depth] < right.segments[depth] ? -1 : 1\n      }\n      return left.segments.length - right.segments.length\n    }\n  }, [currentPath, indexFiles, indexFolders, sort])\n  const preparedInput = React.useMemo(\n    () => prepareFileTreeInput(relativePaths, { sort: sortComparator }),\n    [relativePaths, sortComparator]\n  )\n  // Inject per-file thumbnails into the tree's shadow DOM as sprite symbols\n  // wrapping an <image>, remapped onto rows by file basename. Files without\n  // a thumbnail resolve through the built-in complete icon set instead — the\n  // same colored file-type icons the other views use. The chevron is\n  // remapped to the Hugeicons arrow so it matches the rest of the component;\n  // the tree's rotation CSS keys off data-icon-name, which remapping keeps.\n  const icons = React.useMemo(() => {\n    const byFileName: Record<\n      string,\n      {\n        name: string\n        viewBox: string\n      }\n    > = {}\n    const symbols: string[] = [\n      `<symbol id=\"file-system-chevron\" viewBox=\"0 0 24 24\"><path d=\"M18 9.00005C18 9.00005 13.5811 15 12 15C10.4188 15 6 9 6 9\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"/></symbol>`,\n    ]\n    let thumbnailCount = 0\n    for (const relativePath of relativePaths) {\n      if (thumbnailCount >= TREE_THUMBNAIL_SPRITE_LIMIT) break\n      const file = index.files.get(`${currentPath}${relativePath}`)\n      const coverUrl = file ? filePreviewUrls(file)[0] : undefined\n      if (!file || !coverUrl) continue\n      const baseName = file.name.toLowerCase()\n      if (byFileName[baseName]) continue\n      const symbolId = `file-system-thumbnail-${symbols.length}`\n      symbols.push(\n        `<symbol id=\"${symbolId}\" viewBox=\"0 0 16 16\"><clipPath id=\"${symbolId}-clip\"><rect width=\"16\" height=\"16\" rx=\"2.5\"/></clipPath><image href=\"${escapeXmlAttribute(coverUrl)}\" width=\"16\" height=\"16\" preserveAspectRatio=\"xMidYMid slice\" clip-path=\"url(#${symbolId}-clip)\"/></symbol>`\n      )\n      byFileName[baseName] = { name: symbolId, viewBox: \"0 0 16 16\" }\n      thumbnailCount += 1\n    }\n    return {\n      byFileName,\n      colored: true,\n      remap: {\n        \"file-tree-icon-chevron\": {\n          name: \"file-system-chevron\",\n          viewBox: \"0 0 24 24\",\n        },\n      },\n      set: \"complete\" as const,\n      spriteSheet: `<svg data-icon-sprite aria-hidden=\"true\" width=\"0\" height=\"0\">${symbols.join(\"\")}</svg>`,\n    }\n  }, [currentPath, index, relativePaths])\n  const { model } = useFileTree({\n    flattenEmptyDirectories: false,\n    icons,\n    initialExpansion: \"closed\",\n    // Remounts (folder changes, manifest updates) keep the active filter.\n    initialSearchQuery: searchQuery || null,\n    initialSelectedPaths: initialSelectedPath ? [initialSelectedPath] : [],\n    itemHeight: 28,\n    overscan: 12,\n    preparedInput,\n    renderRowDecoration: ({ row }) => {\n      const entry =\n        row.kind === \"file\"\n          ? index.files.get(`${currentPath}${row.path}`)\n          : index.folders.get(normalizeFolderPath(`${currentPath}${row.path}`))\n      if (!entry) return null\n      // The decoration lane renders one <span title>; CSS splits it into\n      // aligned Date Modified (::before from title) and Size columns.\n      const dateColumn =\n        formatTimestamp(entry.updatedAt ?? entry.createdAt) ?? \"—\"\n      if (entry.kind === \"folder\") {\n        const childCount = index.children.get(entry.path)?.length\n        return {\n          text:\n            childCount === undefined\n              ? \"—\"\n              : `${childCount} ${childCount === 1 ? \"item\" : \"items\"}`,\n          title: dateColumn,\n        }\n      }\n      return { text: formatByteSize(entry.size) ?? \"—\", title: dateColumn }\n    },\n    unsafeCSS: `\n      button[data-type='item']:not([data-item-selected]):hover {\n        background: color-mix(in oklab, var(--color-accent) 50%, transparent);\n      }\n      button[data-type='item'][data-item-selected] {\n        background: var(--color-primary);\n        color: var(--color-primary-foreground);\n        /* The primary surface is the opposite of the mode's background, so\n           the row's light-dark() icon colors resolve against the opposite\n           scheme — light-palette icons on the light pill in dark mode and\n           vice versa. */\n        color-scheme: var(--fs-selected-color-scheme, normal);\n      }\n      button[data-type='item'][data-item-selected] *:not([data-icon-token]):not([data-icon-token] *),\n      button[data-type='item'][data-item-selected] [data-item-section]::before {\n        color: var(--color-primary-foreground) !important;\n      }\n      [data-item-section='decoration'] > span {\n        display: grid;\n        grid-template-columns: 11rem 5rem;\n        white-space: nowrap;\n        /* The size cell is the span's anonymous text item, so alignment\n           rides on text-align: the span's right applies to it while the\n           date cell (::before) overrides back to left. */\n        text-align: right;\n      }\n      [data-item-section='decoration'] > span::before {\n        content: attr(title);\n        text-align: left;\n      }\n      button[data-type='item'][data-item-type='folder'] [data-item-section='content'] {\n        display: flex;\n        align-items: center;\n        min-width: 0;\n      }\n      button[data-type='item'][data-item-type='folder'] [data-item-section='content']::before {\n        content: \"\";\n        flex: none;\n        width: 18px;\n        height: 14px;\n        margin-right: 4px;\n        background: url(\"${FOLDER_GLYPH_DATA_URL}\") center / contain no-repeat;\n      }\n    `,\n    onSelectionChange: (selectedPaths) => {\n      const relativePath = selectedPaths[0]\n      if (!relativePath) {\n        onSelect(null)\n        return\n      }\n      const absolutePath = `${currentPath}${relativePath}`\n      const entry =\n        index.files.get(absolutePath) ??\n        index.folders.get(normalizeFolderPath(absolutePath)) ??\n        null\n      onSelect(entry)\n    },\n  })\n  // Thumbnails can resolve after mount (e.g. generated client-side); push\n  // sprite updates into the existing model instead of remounting the tree.\n  React.useEffect(() => {\n    model.setIcons(icons)\n  }, [icons, model])\n  // The folders currently expanded in the mounted model, derived from the\n  // given path list (the model knows the rows; the paths name the\n  // directories to ask about).\n  const collectExpandedDirectories = React.useCallback(\n    (paths: readonly string[]) => {\n      const expandedPaths: string[] = []\n      for (const directoryPath of directoryPathsOf(paths)) {\n        const item =\n          model.getItem(directoryPath) ?? model.getItem(`${directoryPath}/`)\n        if (item && \"isExpanded\" in item && item.isExpanded()) {\n          expandedPaths.push(directoryPath)\n        }\n      }\n      return expandedPaths\n    },\n    [model]\n  )\n  // Opens every given folder on the mounted model (no-ops on the already\n  // open ones).\n  const expandDirectories = React.useCallback(\n    (directoryPaths: Iterable<string>) => {\n      for (const directoryPath of directoryPaths) {\n        const item =\n          model.getItem(directoryPath) ?? model.getItem(`${directoryPath}/`)\n        if (item && \"isExpanded\" in item && !item.isExpanded()) {\n          item.toggle()\n        }\n      }\n    },\n    [model]\n  )\n  // Sort and filter changes swap the prepared input in place — remounting\n  // would reset every folder's disclosure. The folders expanded in the\n  // outgoing path list, the selection, and the active search query are\n  // captured first and handed back to the reset.\n  const appliedPreparedInputRef = React.useRef(preparedInput)\n  // Filter bookkeeping: the latest prop (for unmount-time decisions), the\n  // state at the last applied reset (for transition detection), and the\n  // disclosure to restore once the filters clear.\n  const hasActiveFiltersRef = React.useRef(hasActiveFilters)\n  const filteredAtLastResetRef = React.useRef(hasActiveFilters)\n  const preFilterExpansionRef = React.useRef<readonly string[] | null>(null)\n  React.useEffect(() => {\n    hasActiveFiltersRef.current = hasActiveFilters\n  })\n  React.useEffect(() => {\n    const previousPreparedInput = appliedPreparedInputRef.current\n    if (previousPreparedInput === preparedInput) return\n    appliedPreparedInputRef.current = preparedInput\n    const wasFiltered = filteredAtLastResetRef.current\n    filteredAtLastResetRef.current = hasActiveFilters\n    // Filters reveal their matches the way the search session does: every\n    // folder on the way to a match opens. The disclosure from just before\n    // filtering is kept aside and comes back when the filters clear.\n    let expandedPaths: readonly string[]\n    if (hasActiveFilters) {\n      if (!wasFiltered) {\n        preFilterExpansionRef.current = collectExpandedDirectories(\n          previousPreparedInput.paths\n        )\n      }\n      expandedPaths = [...directoryPathsOf(preparedInput.paths)]\n    } else if (wasFiltered) {\n      expandedPaths = preFilterExpansionRef.current ?? []\n      preFilterExpansionRef.current = null\n    } else {\n      expandedPaths = collectExpandedDirectories(previousPreparedInput.paths)\n    }\n    const searchValue = model.getSearchValue()\n    // The `paths` argument must stay unset: when both are given, resetPaths\n    // re-prepares the paths with the comparator the model was CREATED with\n    // and rejects the differently-ordered prepared input. Passing only the\n    // prepared input makes the reset adopt its path list as-is, and the\n    // reset itself carries the selection over.\n    model.resetPaths(undefined as unknown as readonly string[], {\n      initialExpandedPaths: expandedPaths,\n      preparedInput,\n    })\n    if (searchValue) model.setSearch(searchValue)\n  }, [collectExpandedDirectories, hasActiveFilters, model, preparedInput])\n  // View switches and navigation unmount the tree; remember which folders\n  // were left expanded and reopen them on the next mount of this folder\n  // (before paint, so the restored disclosure never flashes closed). While\n  // filters are active their matches are revealed instead, and the\n  // remembered disclosure is the pre-filter one.\n  React.useLayoutEffect(() => {\n    const expansionStore = treeExpansionRef.current\n    const savedExpansion = expansionStore.get(currentPath) ?? []\n    if (hasActiveFiltersRef.current) {\n      preFilterExpansionRef.current = savedExpansion\n      expandDirectories(directoryPathsOf(appliedPreparedInputRef.current.paths))\n    } else {\n      expandDirectories(savedExpansion)\n    }\n    return () => {\n      expansionStore.set(\n        currentPath,\n        hasActiveFiltersRef.current\n          ? (preFilterExpansionRef.current ?? [])\n          : collectExpandedDirectories(appliedPreparedInputRef.current.paths)\n      )\n    }\n  }, [\n    collectExpandedDirectories,\n    currentPath,\n    expandDirectories,\n    treeExpansionRef,\n  ])\n  // The toolbar search drives the tree's own search session, which filters\n  // rows with hide-non-matches semantics and highlights the matched text.\n  React.useEffect(() => {\n    model.setSearch(searchQuery || null)\n  }, [model, searchQuery])\n  // The tree's arrow keys move focus and only select on click/Enter; mirror\n  // focus into the (single) selection so arrowing selects like Finder. Shift\n  // ranges keep the focused row selected, so they pass through untouched.\n  React.useEffect(() => {\n    let lastFocusedPath = model.getFocusedPath()\n    return model.subscribe(() => {\n      const focusedPath = model.getFocusedPath()\n      if (focusedPath === lastFocusedPath) return\n      lastFocusedPath = focusedPath\n      if (!focusedPath) return\n      const item = model.getItem(focusedPath)\n      if (!item || item.isSelected()) return\n      for (const path of model.getSelectedPaths()) {\n        model.getItem(path)?.deselect()\n      }\n      item.select()\n    })\n  }, [model])\n  // Rows live in the tree's shadow DOM; composedPath surfaces the row\n  // element behind a pointer or keyboard event so it can resolve to a\n  // manifest entry.\n  const entryFromEvent = (event: React.SyntheticEvent) => {\n    for (const target of event.nativeEvent.composedPath()) {\n      if (!(target instanceof HTMLElement)) continue\n      const relativePath = target.dataset?.itemPath\n      if (!relativePath) continue\n      const absolutePath = `${currentPath}${relativePath}`\n      return (\n        index.files.get(absolutePath) ??\n        index.folders.get(normalizeFolderPath(absolutePath)) ??\n        null\n      )\n    }\n    return null\n  }\n  // The tree exposes rows by relative path; directory ids may or may not\n  // carry the trailing slash depending on the call site.\n  const resolveTreeItem = (relativePath: string) =>\n    model.getItem(relativePath) ??\n    model.getItem(\n      relativePath.endsWith(\"/\")\n        ? relativePath.slice(0, -1)\n        : `${relativePath}/`\n    )\n  // The tree's rows in display order — folders first per level, recursing\n  // only into expanded folders — so type-ahead cycles exactly what's on\n  // screen. Virtualization keeps this off the DOM; the index and the item\n  // handles carry the same information.\n  const collectVisibleEntries = () => {\n    const visibleEntries: FileSystemEntry[] = []\n    const walk = (folderPath: string) => {\n      const children = index.children.get(folderPath) ?? []\n      for (const child of children) {\n        if (child.kind !== \"folder\") continue\n        const item = resolveTreeItem(child.path.slice(currentPath.length))\n        if (!item) continue\n        visibleEntries.push(child)\n        if (\"isExpanded\" in item && item.isExpanded()) walk(child.path)\n      }\n      for (const child of children) {\n        if (child.kind === \"file\") visibleEntries.push(child)\n      }\n    }\n    walk(currentPath)\n    return visibleEntries\n  }\n  const typeAhead = useEntryTypeAhead()\n  return (\n    <PierreFileTree\n      model={model}\n      className=\"block min-h-0 flex-1\"\n      // Finder semantics: double-clicking a folder navigates into it and\n      // double-clicking a file opens it; a single click still only toggles\n      // the folder's disclosure.\n      onDoubleClick={(event) => {\n        const entry = entryFromEvent(event)\n        if (entry) onOpen(entry)\n      }}\n      // Enter mirrors the other views: navigate into the focused folder or\n      // open the focused file. Printable keys run the shared type-ahead\n      // over the visible rows.\n      onKeyDown={(event) => {\n        if (event.key === \"Enter\") {\n          const entry = entryFromEvent(event)\n          if (entry) {\n            event.preventDefault()\n            onOpen(entry)\n          }\n          return\n        }\n        if (!isTypeAheadKey(event)) return\n        const visibleEntries = collectVisibleEntries()\n        const focusedPath = model.getFocusedPath()?.replace(/\\/$/, \"\") ?? null\n        const focusedIndex = visibleEntries.findIndex(\n          (entry) =>\n            entry.path.slice(currentPath.length).replace(/\\/$/, \"\") ===\n            focusedPath\n        )\n        const match = typeAhead(event, visibleEntries, focusedIndex)\n        if (!match) return\n        const item = resolveTreeItem(match.path.slice(currentPath.length))\n        if (item) {\n          model.scrollToPath(item.getPath())\n          item.focus()\n        }\n      }}\n      style={\n        {\n          \"--trees-bg-override\": \"transparent\",\n          \"--trees-border-color-override\": \"var(--color-border)\",\n          \"--trees-fg-override\": \"var(--color-foreground)\",\n          // Match the focus-visible ring used by the tabs and the other\n          // views (`ring-2 ring-ring`) instead of the tree's accent blue.\n          \"--trees-focus-ring-color-override\": \"var(--color-ring)\",\n          \"--trees-focus-ring-width-override\": \"2px\",\n          \"--trees-selected-bg-override\": \"var(--color-primary)\",\n          \"--trees-selected-focused-border-color-override\": \"var(--color-ring)\",\n        } as React.CSSProperties\n      }\n    />\n  )\n}\nfunction FileSystemColumnsView(props: FileSystemViewProps) {\n  const {\n    currentPath,\n    index,\n    loadPreviewImageUrl,\n    loadingFolders,\n    onOpen,\n    onSelect,\n    pageUrlCache,\n    renderFilePreview,\n    selectedEntry,\n    selectedPath,\n  } = props\n  const scrollContainerRef = React.useRef<HTMLDivElement | null>(null)\n  const rowRefs = React.useRef(new Map<string, HTMLButtonElement>())\n  // The selection highlight tracks every keypress; mounting the trailing\n  // child column and the preview pane is deferred so holding an arrow key\n  // doesn't pay that DOM churn per step.\n  const deferredSelectedEntry = React.useDeferredValue(selectedEntry)\n  const deferredSelectedPath = React.useDeferredValue(selectedPath)\n  const pendingFocusPathRef = React.useRef<string | null>(null)\n  const typeAhead = useEntryTypeAhead()\n  const handleKeyDown = (event: React.KeyboardEvent) => {\n    if (!ARROW_KEYS.has(event.key)) {\n      // Type-ahead moves within the active column's rows, like Finder.\n      const siblings =\n        selectedEntry && selectedPath?.startsWith(currentPath)\n          ? (index.children.get(selectedEntry.parentPath) ?? [])\n          : (index.children.get(currentPath) ?? [])\n      const match = typeAhead(\n        event,\n        siblings,\n        siblings.findIndex((sibling) => sibling.path === selectedPath)\n      )\n      if (match) {\n        onSelect(match)\n        const row = rowRefs.current.get(match.path)\n        if (row) {\n          row.focus()\n        } else {\n          pendingFocusPathRef.current = match.path\n        }\n      }\n      return\n    }\n    let nextEntry: FileSystemEntry | null | undefined\n    if (!selectedEntry || !selectedPath?.startsWith(currentPath)) {\n      nextEntry = index.children.get(currentPath)?.[0]\n    } else if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n      const siblings = index.children.get(selectedEntry.parentPath) ?? []\n      const currentIndex = siblings.findIndex(\n        (sibling) => sibling.path === selectedEntry.path\n      )\n      nextEntry = siblings[currentIndex + (event.key === \"ArrowUp\" ? -1 : 1)]\n    } else if (event.key === \"ArrowLeft\") {\n      if (selectedEntry.parentPath !== currentPath) {\n        nextEntry = index.folders.get(selectedEntry.parentPath)\n      }\n    } else if (selectedEntry.kind === \"folder\") {\n      nextEntry = index.children.get(selectedEntry.path)?.[0]\n    }\n    if (!nextEntry) return\n    onSelect(nextEntry)\n    const row = rowRefs.current.get(nextEntry.path)\n    if (row) {\n      pendingFocusPathRef.current = null\n      row.focus()\n    } else {\n      // The target row lives in a deferred column that hasn't mounted yet;\n      // focus it from the effect below once it exists.\n      pendingFocusPathRef.current = nextEntry.path\n    }\n    event.preventDefault()\n  }\n  React.useEffect(() => {\n    const path = pendingFocusPathRef.current\n    if (!path) return\n    const row = rowRefs.current.get(path)\n    if (row) {\n      pendingFocusPathRef.current = null\n      row.focus()\n    }\n  })\n  const columnPaths = React.useMemo(() => {\n    const paths = [currentPath]\n    if (!deferredSelectedPath?.startsWith(currentPath)) return paths\n    const targetFolder =\n      deferredSelectedEntry?.kind === \"folder\"\n        ? deferredSelectedEntry.path\n        : (deferredSelectedEntry?.parentPath ?? currentPath)\n    const relativePath = targetFolder.slice(currentPath.length)\n    let walkedPath = currentPath\n    for (const segment of relativePath.split(\"/\")) {\n      if (!segment) continue\n      walkedPath = `${walkedPath}${segment}/`\n      paths.push(walkedPath)\n    }\n    return paths\n  }, [currentPath, deferredSelectedEntry, deferredSelectedPath])\n  // Roving tabindex: all columns together form a single tab stop (the\n  // selected row when its column is mounted, else the first row), so\n  // Shift+Tab returns to the toolbar like in the list view.\n  const tabStopPath = React.useMemo(() => {\n    if (selectedPath) {\n      for (const columnPath of columnPaths) {\n        if (\n          index.children\n            .get(columnPath)\n            ?.some((entry) => entry.path === selectedPath)\n        ) {\n          return selectedPath\n        }\n      }\n    }\n    return index.children.get(columnPaths[0] ?? \"\")?.[0]?.path ?? null\n  }, [columnPaths, index, selectedPath])\n  const selectedFile =\n    deferredSelectedEntry?.kind === \"file\"\n      ? (deferredSelectedEntry as FileEntry)\n      : null\n  const selectedFileSize = selectedFile\n    ? formatByteSize(selectedFile.size)\n    : null\n  React.useEffect(() => {\n    const container = scrollContainerRef.current\n    if (container) container.scrollLeft = container.scrollWidth\n  }, [columnPaths.length, deferredSelectedPath])\n  return (\n    <InlineScrollArea2\n      orientation=\"horizontal\"\n      viewportRef={scrollContainerRef}\n      viewportClassName=\"overscroll-x-contain\"\n    >\n      {/* The Content part's ResizeObserver tells the scroll area when the\n            trail shrinks (deselect, shallower selection) so the horizontal\n            scrollbar hides; the viewport alone only observes its own box. Its\n            built-in inline min-width (fit-content) would beat a min-w-full\n            class, so the full-width floor is inline too. */}\n      <InlineScrollAreaContent\n        className=\"flex h-full w-max\"\n        style={{ minWidth: \"100%\" }}\n        onKeyDown={handleKeyDown}\n      >\n        {columnPaths.map((columnPath, columnIndex) => (\n          <FileSystemColumn\n            key={columnPath || \"(root)\"}\n            entries={index.children.get(columnPath) ?? []}\n            index={index}\n            isLoading={loadingFolders.has(columnPath)}\n            onOpen={onOpen}\n            onSelect={onSelect}\n            rowRefs={rowRefs}\n            // Scalar per-column props so the memoized column only\n            // re-renders when its own rows change — a selection deeper in\n            // the trail leaves ancestor columns untouched.\n            selectedChildPath={\n              selectedPath && pathParent(selectedPath) === columnPath\n                ? selectedPath\n                : null\n            }\n            tabStopChildPath={\n              tabStopPath && pathParent(tabStopPath) === columnPath\n                ? tabStopPath\n                : null\n            }\n            trailChildPath={columnPaths[columnIndex + 1] ?? null}\n          />\n        ))}\n        {selectedFile ? (\n          <InlineScrollArea2\n            orientation=\"vertical\"\n            className=\"min-w-60 flex-1 contain-inline-size\"\n            viewportClassName=\"flex justify-center p-4\"\n          >\n            <div className=\"flex w-full max-w-lg flex-col items-stretch gap-3\">\n              {/* Width derives from the aspect ratio so the thumbnail grows\n                with the pane up to a 20rem height cap. */}\n              <div\n                className=\"mx-auto w-full shrink-0\"\n                style={{\n                  maxWidth: `min(100%, ${(selectedFile.previewAspectRatio ?? 0.78) * 20}rem)`,\n                }}\n              >\n                <FileVisual\n                  file={selectedFile}\n                  className=\"w-full\"\n                  loadPreviewImageUrl={loadPreviewImageUrl}\n                  pageable\n                  pageUrlCache={pageUrlCache}\n                  previewAspectRatio={0.78}\n                  renderFilePreview={renderFilePreview}\n                />\n              </div>\n              <div className=\"text-center\">\n                <div className=\"text-sm font-semibold break-words\">\n                  {selectedFile.name}\n                </div>\n                <div className=\"text-xs text-muted-foreground\">\n                  {fileKindLabel(selectedFile)}\n                  {selectedFileSize ? ` - ${selectedFileSize}` : null}\n                </div>\n              </div>\n              <FileSystemInformation entry={selectedFile} index={index} />\n            </div>\n          </InlineScrollArea2>\n        ) : null}\n      </InlineScrollAreaContent>\n    </InlineScrollArea2>\n  )\n}\n// Column row geometry (px at the default 16px root font size).\nconst COLUMN_PADDING = 6 // p-1.5\nconst COLUMN_ROW_HEIGHT = 28 // h-7\nconst COLUMN_ROW_GAP = 1 // gap-px\nconst COLUMN_ROW_STRIDE = COLUMN_ROW_HEIGHT + COLUMN_ROW_GAP\n// Memoized with scalar selection props: pressing into a deep trail only\n// re-renders the columns whose rows actually change.\nconst FileSystemColumn = React.memo(function FileSystemColumn({\n  entries,\n  index,\n  isLoading,\n  onOpen,\n  onSelect,\n  rowRefs,\n  selectedChildPath,\n  tabStopChildPath,\n  trailChildPath,\n}: {\n  entries: FileSystemEntry[]\n  index: FileSystemIndex\n  isLoading: boolean\n  onOpen: (entry: FileSystemEntry) => void\n  onSelect: (entry: FileSystemEntry | null) => void\n  rowRefs: React.RefObject<Map<string, HTMLButtonElement>>\n  selectedChildPath: string | null\n  tabStopChildPath: string | null\n  trailChildPath: string | null\n}) {\n  const viewportRef = React.useRef<HTMLDivElement | null>(null)\n  const { end, start } = useVirtualWindow({\n    count: entries.length,\n    itemStride: COLUMN_ROW_STRIDE,\n    leadingPx: COLUMN_PADDING,\n    overscan: 10,\n    viewportRef,\n  })\n  // Keyboard navigation can select a row this column hasn't mounted; scroll\n  // it into the viewport so it mounts and the pending-focus effect can land.\n  React.useLayoutEffect(() => {\n    if (!selectedChildPath) return\n    scrollIndexIntoView({\n      index: entries.findIndex((entry) => entry.path === selectedChildPath),\n      itemSize: COLUMN_ROW_HEIGHT,\n      itemStride: COLUMN_ROW_STRIDE,\n      leadingPx: COLUMN_PADDING,\n      viewport: viewportRef.current,\n    })\n  }, [entries, selectedChildPath])\n  return (\n    <InlineScrollArea2\n      orientation=\"vertical\"\n      className=\"w-60 shrink-0 border-r\"\n      viewportRef={viewportRef}\n      viewportClassName=\"p-1.5\"\n      viewportProps={{ \"aria-label\": \"Files\", role: \"listbox\" }}\n    >\n      {isLoading && entries.length === 0 ? (\n        <div className=\"animate-pulse px-2 py-1.5 text-xs text-muted-foreground motion-reduce:animate-none\">\n          Loading…\n        </div>\n      ) : (\n        <div\n          className=\"relative\"\n          style={{\n            height: entries.length\n              ? entries.length * COLUMN_ROW_STRIDE - COLUMN_ROW_GAP\n              : undefined,\n          }}\n        >\n          <div\n            className=\"absolute inset-x-0 flex flex-col gap-px\"\n            style={{ top: start * COLUMN_ROW_STRIDE }}\n          >\n            {entries.slice(start, end).map((entry) => {\n              const isSelected = entry.path === selectedChildPath\n              const isOnTrail =\n                entry.kind === \"folder\" && entry.path === trailChildPath\n              const coverUrl =\n                entry.kind === \"file\" ? filePreviewUrls(entry)[0] : undefined\n              return (\n                <button\n                  key={entry.path}\n                  type=\"button\"\n                  role=\"option\"\n                  aria-selected={isSelected}\n                  // Selected rows sit on the primary surface — the opposite\n                  // of the mode's background — so the file-type icon swaps\n                  // to the opposite palette.\n                  data-file-system-on-primary={isSelected ? \"\" : undefined}\n                  tabIndex={entry.path === tabStopChildPath ? 0 : -1}\n                  ref={(element) => {\n                    if (element) {\n                      rowRefs.current.set(entry.path, element)\n                    } else {\n                      rowRefs.current.delete(entry.path)\n                    }\n                  }}\n                  // Selecting on press (mouse only) starts mounting the\n                  // child column a beat before mouseup — the immediacy\n                  // @pierre/trees rows have. Touch keeps selection on the\n                  // click so scroll gestures don't select.\n                  onPointerDown={(event) => {\n                    if (event.pointerType === \"mouse\" && event.button === 0) {\n                      onSelect(entry)\n                    }\n                  }}\n                  onClick={() => onSelect(entry)}\n                  onDoubleClick={() => onOpen(entry)}\n                  onKeyDown={(event) => {\n                    if (event.key === \"Enter\") onOpen(entry)\n                  }}\n                  className={cn(\n                    \"flex h-7 shrink-0 items-center gap-2 rounded-md px-2 py-1 text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                    isSelected\n                      ? \"bg-primary text-primary-foreground\"\n                      : isOnTrail\n                        ? \"bg-accent\"\n                        : \"hover:bg-accent/50\"\n                  )}\n                >\n                  {entry.kind === \"folder\" ? (\n                    <FileSystemFolderGlyph className=\"h-3.5 w-auto shrink-0\" />\n                  ) : coverUrl ? (\n                    // eslint-disable-next-line @next/next/no-img-element -- Cover thumbnails come from caller-provided file preview URLs.\n                    <img\n                      src={coverUrl}\n                      alt=\"\"\n                      draggable={false}\n                      className=\"size-4 shrink-0 rounded-[3px] bg-white object-cover\"\n                    />\n                  ) : (\n                    <FileTypeIcon\n                      fileName={entry.name}\n                      className=\"size-4 shrink-0\"\n                    />\n                  )}\n                  <span className=\"min-w-0 flex-1 truncate\">{entry.name}</span>\n                  {entry.kind === \"folder\" &&\n                  folderHasChildren(index, entry) ? (\n                    <IconPlaceholder\n                      lucide=\"ArrowRight\"\n                      tabler=\"IconArrowRight\"\n                      hugeicons=\"ArrowRight01Icon\"\n                      phosphor=\"ArrowRightIcon\"\n                      remixicon=\"RiArrowRightLine\"\n                      className={cn(\n                        \"size-3.5 shrink-0\",\n                        !isSelected && \"text-muted-foreground/60\"\n                      )}\n                    />\n                  ) : null}\n                </button>\n              )\n            })}\n          </div>\n        </div>\n      )}\n    </InlineScrollArea2>\n  )\n})\nfunction FileSystemInformation({\n  entry,\n  index,\n}: {\n  entry: FileSystemEntry\n  index: FileSystemIndex\n}) {\n  const rows: Array<[string, string]> = []\n  const created = formatTimestamp(entry.createdAt)\n  const updated = formatTimestamp(entry.updatedAt)\n  if (created) rows.push([\"Created\", created])\n  if (updated) rows.push([\"Modified\", updated])\n  if (entry.kind === \"file\") {\n    const size = formatByteSize(entry.size)\n    if (size) rows.push([\"Size\", size])\n  } else {\n    const childCount = index.children.get(entry.path)?.length\n    if (childCount !== undefined) {\n      rows.push([\"Items\", `${childCount}`])\n    }\n  }\n  if (rows.length === 0) return null\n  return (\n    <div className=\"border-t pt-3\">\n      <div className=\"mb-1.5 text-xs font-semibold\">Information</div>\n      <dl className=\"space-y-1\">\n        {rows.map(([label, value]) => (\n          <div\n            key={label}\n            className=\"flex items-baseline justify-between gap-3 text-xs\"\n          >\n            <dt className=\"shrink-0 text-muted-foreground\">{label}</dt>\n            <dd className=\"text-right\" suppressHydrationWarning>\n              {value}\n            </dd>\n          </div>\n        ))}\n      </dl>\n    </div>\n  )\n}\n// Filmstrip geometry (px at the default 16px root font size).\nconst GALLERY_STRIP_PADDING = 8 // p-2\nconst GALLERY_TILE_SIZE = 56 // size-14\nconst GALLERY_TILE_GAP = 6 // gap-1.5\nconst GALLERY_TILE_STRIDE = GALLERY_TILE_SIZE + GALLERY_TILE_GAP\n// How many visited stages stay mounted so stepping back to a recent file\n// restores its already-loaded preview without refetching or re-parsing;\n// also bounds the memory the keep-alive pool can hold onto.\nconst GALLERY_STAGE_POOL_SIZE = 4\n// Of those, how many stay attached to the DOM (the active stage plus the\n// two before it, keeping the usual two-or-three-file rotation instant).\n// The rest wait detached, costing no layout or style-recalc work until\n// they return — their page canvases remount on the way back, so returning\n// to a detached stage briefly rebuilds the page content.\nconst GALLERY_STAGE_ATTACHED_COUNT = 3\n// The preview for one pooled file. Each stage owns its URL resolution and\n// viewer state, so a mounted stage is self-contained: the root keeps\n// recently shown stages alive (reparented between hosts rather than\n// remounted, because the document viewers load in effects and would\n// refetch and re-parse on remount) and revisiting one — in the gallery or\n// the dialog — skips the presign, download, and parse work instead of\n// re-running it behind a spinner. The two variants share one element\n// structure, differing only in props and classes, so flipping a mounted\n// stage between them keeps the viewer instance.\nfunction FileSystemGalleryStage({\n  file,\n  getFileUrl,\n  loadPreviewImageUrl,\n  pageUrlCache,\n  renderFilePreview,\n  toolbarActions,\n  urlCache,\n  variant = \"stage\",\n}: {\n  file: FileEntry\n  getFileUrl?: (file: FileSystemFileItem) => string | Promise<string>\n  loadPreviewImageUrl?: (\n    file: FileSystemFileItem,\n    pageIndex: number\n  ) => Promise<string | null>\n  pageUrlCache?: Map<string, string>\n  renderFilePreview?: (file: FileSystemFileItem) => React.ReactNode\n  /** Rendered in the viewer toolbar in the `\"dialog\"` variant. */\n  toolbarActions?: React.ReactNode\n  urlCache: Map<string, string>\n  /** `\"stage\"` is toolbar-less in a bordered tile; `\"dialog\"` shows the full viewer chrome. */\n  variant?: \"dialog\" | \"stage\"\n}) {\n  const viewerKind = viewerKindForFile(file)\n  // Only viewer-backed stages need a URL; thumbnail stages render from the\n  // manifest's preview images, so selecting them never triggers a presign.\n  const { isResolving, url } = useResolvedFileUrl(\n    viewerKind ? file : null,\n    getFileUrl,\n    urlCache\n  )\n  const isDialog = variant === \"dialog\"\n  const [isDark, setIsDark] = React.useState(false)\n  const viewerFrameClassName = cn(\n    \"size-full\",\n    !isDialog && \"overflow-hidden rounded-lg border\"\n  )\n  if (viewerKind && isResolving) {\n    return <InlineSpinner className=\"size-6 text-muted-foreground\" />\n  }\n  if (viewerKind === \"image\" && url) {\n    return (\n      // eslint-disable-next-line @next/next/no-img-element -- Image file previews render caller-provided URLs that may be object or presigned URLs.\n      <img\n        src={url}\n        alt={file.name}\n        className=\"max-h-full max-w-full rounded-lg object-contain\"\n      />\n    )\n  }\n  if (viewerKind === \"pdf\" && url) {\n    return (\n      <div className={viewerFrameClassName}>\n        <React.Suspense fallback={<FileSystemViewerLoading />}>\n          <LazyPDFViewer\n            src={url}\n            className={cn(\n              \"h-full\",\n              isDialog && \"min-h-0 overflow-hidden rounded-2xl\"\n            )}\n            fileName={file.name}\n            showToolbar={isDialog}\n            showUpload={false}\n            toolbarActions={toolbarActions}\n          />\n        </React.Suspense>\n      </div>\n    )\n  }\n  if (viewerKind === \"docx\" && url) {\n    return (\n      <div className={viewerFrameClassName}>\n        <React.Suspense fallback={<FileSystemViewerLoading />}>\n          <LazyDocxViewerPreview\n            src={url}\n            fileName={file.name}\n            isDark={isDark}\n            className={cn(\n              \"h-full min-h-0\",\n              isDialog && \"overflow-hidden rounded-2xl\"\n            )}\n            onIsDarkChange={setIsDark}\n            showToolbar={isDialog}\n            showUpload={false}\n            toolbarActions={toolbarActions}\n          />\n        </React.Suspense>\n      </div>\n    )\n  }\n  if (viewerKind === \"pptx\" && url) {\n    return (\n      <div className={viewerFrameClassName}>\n        <React.Suspense fallback={<FileSystemViewerLoading />}>\n          <LazyPptxViewerPreview\n            src={url}\n            fileName={file.name}\n            defaultThumbnailSidebarOpen={false}\n            className={cn(\n              \"h-full min-h-0\",\n              isDialog && \"overflow-hidden rounded-2xl\"\n            )}\n            showToolbar={isDialog}\n            showUpload={false}\n            toolbarActions={toolbarActions}\n          />\n        </React.Suspense>\n      </div>\n    )\n  }\n  if (viewerKind === \"xlsx\" && url) {\n    return (\n      <div className={viewerFrameClassName}>\n        <React.Suspense fallback={<FileSystemViewerLoading />}>\n          <LazyXlsxViewerPreview\n            src={url}\n            fileName={file.name}\n            isDark={isDark}\n            className={cn(\n              \"h-full min-h-0\",\n              isDialog && \"overflow-hidden rounded-2xl\"\n            )}\n            onIsDarkChange={setIsDark}\n            showToolbar={isDialog}\n            showUpload={false}\n            toolbarActions={toolbarActions}\n          />\n        </React.Suspense>\n      </div>\n    )\n  }\n  return (\n    <FileVisual\n      file={file}\n      className=\"w-56 max-w-full\"\n      loadPreviewImageUrl={loadPreviewImageUrl}\n      pageable\n      pageUrlCache={pageUrlCache}\n      previewAspectRatio={0.78}\n      renderFilePreview={renderFilePreview}\n    />\n  )\n}\nfunction FileSystemGalleryView(props: FileSystemViewProps) {\n  const {\n    attachedStagePaths,\n    entries,\n    index,\n    onOpen,\n    onSelect,\n    poolStagePath,\n    registerStageHost,\n    renderFilePreview,\n    selectedEntry,\n    selectedPath,\n  } = props\n  const stripRefs = React.useRef(new Map<string, HTMLButtonElement>())\n  const stripViewportRef = React.useRef<HTMLDivElement | null>(null)\n  const typeAhead = useEntryTypeAhead()\n  const activeEntry =\n    selectedEntry && entries.some((entry) => entry.path === selectedEntry.path)\n      ? selectedEntry\n      : (entries[0] ?? null)\n  const activeFile = activeEntry?.kind === \"file\" ? activeEntry : null\n  // While arrow keys are scrubbing the strip, the center pane shows a\n  // spinner; a file is only admitted to the preview pool (mounting its\n  // viewer and resolving its URL) once the selection settles so each\n  // keystroke stays cheap.\n  const settledPath = useSettledValue(activeEntry?.path ?? null, 200)\n  React.useEffect(() => {\n    if (settledPath) poolStagePath(settledPath)\n  }, [poolStagePath, settledPath])\n  // Hosts for the root-owned preview pool: one positioned wrapper per\n  // pooled path; the root reparents each live preview into its wrapper.\n  // Stable callbacks per path keep React from re-running the host refs on\n  // unrelated renders.\n  const stageHostRefs = React.useMemo(\n    () =>\n      new Map(\n        attachedStagePaths.map(\n          (path) =>\n            [\n              path,\n              (element: HTMLElement | null) => registerStageHost(path, element),\n            ] as const\n        )\n      ),\n    [attachedStagePaths, registerStageHost]\n  )\n  const activeFileSize = activeFile ? formatByteSize(activeFile.size) : null\n  const handleKeyDown = (event: React.KeyboardEvent) => {\n    if (entries.length === 0) return\n    const currentIndex = activeEntry\n      ? entries.findIndex((entry) => entry.path === activeEntry.path)\n      : -1\n    if (event.key !== \"ArrowLeft\" && event.key !== \"ArrowRight\") {\n      const match = typeAhead(event, entries, currentIndex)\n      if (match) {\n        onSelect(match)\n        // The matched tile may be outside the strip's virtual window; the\n        // active-tile effect scrolls it in, and focus follows once mounted.\n        requestAnimationFrame(() => stripRefs.current.get(match.path)?.focus())\n      }\n      return\n    }\n    const nextEntry =\n      entries[\n        currentIndex === -1\n          ? 0\n          : currentIndex + (event.key === \"ArrowLeft\" ? -1 : 1)\n      ]\n    if (!nextEntry) return\n    onSelect(nextEntry)\n    stripRefs.current.get(nextEntry.path)?.focus()\n    event.preventDefault()\n  }\n  const { end: stripEnd, start: stripStart } = useVirtualWindow({\n    count: entries.length,\n    horizontal: true,\n    itemStride: GALLERY_TILE_STRIDE,\n    leadingPx: GALLERY_STRIP_PADDING,\n    overscan: 8,\n    viewportRef: stripViewportRef,\n  })\n  // Keep the active tile mounted and visible while scrubbing or when the\n  // selection arrives from another view.\n  const activePath = activeEntry?.path ?? null\n  React.useLayoutEffect(() => {\n    if (!activePath) return\n    scrollIndexIntoView({\n      horizontal: true,\n      index: entries.findIndex((entry) => entry.path === activePath),\n      itemSize: GALLERY_TILE_SIZE,\n      itemStride: GALLERY_TILE_STRIDE,\n      leadingPx: GALLERY_STRIP_PADDING,\n      viewport: stripViewportRef.current,\n    })\n  }, [activePath, entries])\n  return (\n    <div className=\"flex size-full flex-col\" onKeyDown={handleKeyDown}>\n      {/* The strip comes first in DOM order (rendered below via order-last)\n            so the filmstrip is the view's single tab stop: Shift+Tab exits to\n            the toolbar instead of landing inside the embedded viewers. */}\n      <InlineScrollArea2\n        orientation=\"horizontal\"\n        className=\"order-last h-auto w-full shrink-0 border-t\"\n        viewportRef={stripViewportRef}\n        viewportClassName=\"p-2\"\n      >\n        <div\n          className=\"relative h-14 min-w-full\"\n          style={{\n            width: entries.length\n              ? entries.length * GALLERY_TILE_STRIDE - GALLERY_TILE_GAP\n              : undefined,\n          }}\n        >\n          <div\n            role=\"listbox\"\n            aria-label=\"Files\"\n            className=\"absolute inset-y-0 flex items-center gap-1.5\"\n            style={{ left: stripStart * GALLERY_TILE_STRIDE }}\n          >\n            {entries.slice(stripStart, stripEnd).map((entry) => {\n              const isActive =\n                entry.path === (activeEntry?.path ?? selectedPath)\n              return (\n                <button\n                  key={entry.path}\n                  type=\"button\"\n                  role=\"option\"\n                  aria-selected={isActive}\n                  tabIndex={isActive ? 0 : -1}\n                  ref={(element) => {\n                    if (element) {\n                      stripRefs.current.set(entry.path, element)\n                    } else {\n                      stripRefs.current.delete(entry.path)\n                    }\n                  }}\n                  onClick={() => onSelect(entry)}\n                  onDoubleClick={() => onOpen(entry)}\n                  onKeyDown={(event) => {\n                    if (event.key === \"Enter\") onOpen(entry)\n                  }}\n                  title={entry.name}\n                  className={cn(\n                    \"flex size-14 shrink-0 items-center justify-center rounded-md border border-transparent p-1 outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                    isActive && \"border-ring/40 bg-accent\"\n                  )}\n                >\n                  {entry.kind === \"folder\" ? (\n                    <FileSystemFolderGlyph className=\"h-9 w-auto\" />\n                  ) : (\n                    <FileVisual\n                      file={entry}\n                      className=\"w-9 rounded-sm\"\n                      previewAspectRatio={0.78}\n                      renderFilePreview={renderFilePreview}\n                    />\n                  )}\n                </button>\n              )\n            })}\n          </div>\n        </div>\n      </InlineScrollArea2>\n      <div className=\"flex min-h-0 flex-1\">\n        <div className=\"relative flex min-h-0 min-w-0 flex-1 items-center justify-center p-3\">\n          {activeEntry?.kind === \"folder\" ? (\n            <FileSystemFolderGlyph className=\"h-40 max-h-full w-auto drop-shadow-md\" />\n          ) : activeFile && !attachedStagePaths.includes(activeFile.path) ? (\n            <InlineSpinner className=\"size-6 text-muted-foreground\" />\n          ) : null}\n          {/* Inactive hosts hide via `visibility` + `opacity`, never\n            `display`: the document viewers size pages off ResizeObserver\n            measurements, and display:none would collapse them to zero\n            width — every reveal would re-lay-out and re-rasterize behind\n            a blank pane. Stacking absolutely keeps each hidden stage at\n            its real size so revealing one is pure paint. `opacity-0`\n            matters: descendants can override an inherited\n            visibility:hidden with their own visibility:visible (the\n            spreadsheet grid's cell-selection overlay does), but nothing\n            can opt out of an ancestor's zero opacity. `inert` keeps the\n            hidden viewer's focusables out of reach. */}\n          {attachedStagePaths.map((path) => {\n            const isActiveStage = path === activeFile?.path\n            return (\n              <div\n                key={path}\n                ref={stageHostRefs.get(path)}\n                inert={!isActiveStage || undefined}\n                className={cn(\n                  \"absolute inset-0 flex items-center justify-center p-3\",\n                  !isActiveStage && \"invisible opacity-0\"\n                )}\n              />\n            )\n          })}\n        </div>\n        {activeEntry ? (\n          <InlineScrollArea2\n            orientation=\"vertical\"\n            className=\"hidden w-64 shrink-0 border-l sm:block\"\n            viewportClassName=\"flex flex-col gap-3 p-4\"\n          >\n            <div className=\"flex items-center gap-3\">\n              {activeFile ? (\n                <FileVisual\n                  file={activeFile}\n                  className={cn(\n                    \"shrink-0 rounded-sm\",\n                    (activeFile.previewAspectRatio ?? 0.78) > 1.2\n                      ? \"w-16\"\n                      : \"w-9\"\n                  )}\n                  previewAspectRatio={0.78}\n                  renderFilePreview={renderFilePreview}\n                />\n              ) : (\n                <FileSystemFolderGlyph className=\"h-8 w-auto shrink-0\" />\n              )}\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"text-sm font-semibold break-words\">\n                  {activeEntry.name}\n                </div>\n                <div className=\"text-xs text-muted-foreground\">\n                  {activeFile ? fileKindLabel(activeFile) : \"Folder\"}\n                  {activeFileSize ? ` - ${activeFileSize}` : null}\n                </div>\n              </div>\n            </div>\n            <FileSystemInformation entry={activeEntry} index={index} />\n          </InlineScrollArea2>\n        ) : null}\n      </div>\n    </div>\n  )\n}\nfunction InlineDialogPanel({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <InlineScrollArea className=\"min-h-0\">\n      <div\n        data-slot=\"dialog-panel\"\n        className={cn(\"min-h-0\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </InlineScrollArea>\n  )\n}\ntype InlineRegistryIconProps = Omit<\n  React.ComponentProps<\"svg\">,\n  \"children\" | \"strokeWidth\"\n> & { strokeWidth?: number }\nfunction InlineScrollArea2({\n  className,\n  children,\n  orientation = \"both\",\n  scrollFade = false,\n  scrollbarGutter = false,\n  scrollbarOverflowOnly = false,\n  viewportClassName,\n  viewportProps,\n  viewportRef,\n  ...props\n}: InlineScrollAreaProps) {\n  const {\n    className: viewportPropsClassName,\n    ref: viewportPropsRef,\n    ...resolvedViewportProps\n  } = viewportProps ?? {}\n  const composedViewportRef = React.useMemo(\n    () => InlineComposeRefs(viewportPropsRef, viewportRef),\n    [viewportPropsRef, viewportRef]\n  )\n\n  if (\n    !viewportProps &&\n    !viewportRef &&\n    !viewportClassName &&\n    !scrollFade &&\n    !scrollbarGutter &&\n    !scrollbarOverflowOnly\n  ) {\n    return (\n      <InlineScrollArea\n        {...props}\n        className={cn(\n          \"size-full min-h-0\",\n          orientation === \"horizontal\" &&\n            \"[&>[data-orientation=vertical]]:hidden\",\n          className\n        )}\n      >\n        {children}\n        {orientation !== \"vertical\" ? (\n          <ScrollBar orientation=\"horizontal\" />\n        ) : null}\n      </InlineScrollArea>\n    )\n  }\n\n  return (\n    <ScrollAreaPrimitive.Root\n      className={cn(\n        \"size-full min-h-0\",\n        scrollbarOverflowOnly &&\n          \"[&:not(:has([data-slot=scroll-area-viewport][data-has-overflow-x]))_[data-orientation=horizontal]]:hidden [&:not(:has([data-slot=scroll-area-viewport][data-has-overflow-y]))_[data-orientation=vertical]]:hidden\",\n        className\n      )}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        {...resolvedViewportProps}\n        ref={composedViewportRef}\n        className={cn(\n          \"h-full rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          scrollFade &&\n            \"mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] [--fade-size:1.5rem]\",\n          scrollbarGutter && orientation !== \"vertical\" && \"pb-3.5\",\n          scrollbarGutter && orientation !== \"horizontal\" && \"pe-3.5\",\n          viewportPropsClassName,\n          viewportClassName\n        )}\n        data-slot=\"scroll-area-viewport\"\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      {orientation !== \"horizontal\" ? (\n        <ScrollBar orientation=\"vertical\" />\n      ) : null}\n      {orientation !== \"vertical\" ? (\n        <ScrollBar orientation=\"horizontal\" />\n      ) : null}\n      {orientation === \"both\" ? <ScrollAreaPrimitive.Corner /> : null}\n    </ScrollAreaPrimitive.Root>\n  )\n}\ntype InlineScrollAreaProps = ScrollAreaPrimitive.Root.Props & {\n  orientation?: \"vertical\" | \"horizontal\" | \"both\"\n  scrollFade?: boolean\n  scrollbarGutter?: boolean\n  scrollbarOverflowOnly?: boolean\n  viewportClassName?: string\n  viewportProps?: ScrollAreaPrimitive.Viewport.Props\n  viewportRef?: React.Ref<HTMLDivElement>\n}\nfunction InlineComposeRefs<T>(...refs: Array<React.Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (!ref) continue\n      if (typeof ref === \"function\") ref(node)\n      else ref.current = node\n    }\n  }\n}\nfunction InlineSpinner({ className, ...props }: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"LoaderCircle\"\n      tabler=\"IconLoader2\"\n      hugeicons=\"Loading03Icon\"\n      phosphor=\"CircleNotchIcon\"\n      remixicon=\"RiLoader4Line\"\n      role=\"status\"\n      aria-label=\"Loading\"\n      className={cn(\"size-4 animate-spin\", className)}\n      {...props}\n    />\n  )\n}\nfunction InlineScrollAreaContent(props: ScrollAreaPrimitive.Content.Props) {\n  return <ScrollAreaPrimitive.Content {...props} />\n}\n",
      "type": "registry:ui",
      "target": "@components/extend/file-system.tsx"
    }
  ],
  "categories": [
    "documents"
  ],
  "type": "registry:ui"
}
