{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-browser",
  "type": "registry:component",
  "title": "File Browser",
  "description": "Folder-aware browser that descends into common prefixes with a breadcrumb trail.",
  "dependencies": [
    "files-sdk",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "https://files-sdk.dev/r/file-actions.json"
  ],
  "files": [
    {
      "path": "registry/files-sdk/file-browser/file-browser.tsx",
      "type": "registry:component",
      "target": "components/files-sdk/file-browser.tsx",
      "content": "\"use client\";\n\nimport type { StoredFile } from \"files-sdk\";\nimport type { UseFilesResult } from \"files-sdk/react\";\nimport {\n  ChevronRightIcon,\n  FileIcon,\n  FolderIcon,\n  HomeIcon,\n  Loader2Icon,\n} from \"lucide-react\";\nimport { Fragment, useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { FileActions } from \"@/components/files-sdk/file-actions\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface FileBrowserProps {\n  /** A `useFiles()` instance — folders and files are listed through it. */\n  files: UseFilesResult;\n  /** Folder to open on mount, e.g. `\"photos/\"`. Defaults to the root. */\n  initialPrefix?: string;\n  /** Delimiter that marks a folder boundary. Default `\"/\"`. */\n  delimiter?: string;\n  /** Called when a file row (not a folder) is clicked. */\n  onSelect?: (file: StoredFile) => void;\n  /** Called after a successful copy/rename/move/delete from a row's actions menu. */\n  onChanged?: () => void;\n  /** Hide the per-file actions menu. */\n  readOnly?: boolean;\n  className?: string;\n}\n\nconst formatBytes = (bytes: number): string => {\n  if (bytes === 0) {\n    return \"0 B\";\n  }\n  const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  const exponent = Math.min(\n    Math.floor(Math.log(bytes) / Math.log(1024)),\n    units.length - 1\n  );\n  return `${(bytes / 1024 ** exponent).toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`;\n};\n\n/** Split `\"photos/2024/\"` into clickable crumbs with their cumulative prefix. */\nconst crumbsOf = (\n  prefix: string,\n  delimiter: string\n): { label: string; prefix: string }[] => {\n  const parts = prefix.split(delimiter).filter(Boolean);\n  let acc = \"\";\n  return parts.map((label) => {\n    acc += label + delimiter;\n    return { label, prefix: acc };\n  });\n};\n\n/** Strip the parent prefix + trailing delimiter so a folder shows its own name. */\nconst folderName = (\n  folderPrefix: string,\n  parent: string,\n  delimiter: string\n): string => folderPrefix.slice(parent.length).replace(delimiter, \"\");\n\n/**\n * A folder-aware browser for a `useFiles()` instance. Uses `list({ delimiter })`\n * so common prefixes surface as folders you can descend into, with a breadcrumb\n * trail and cursor-based \"load more\". Each file row carries a `FileActions` menu\n * (download, copy, rename, move, delete) unless `readOnly`. Falls back\n * gracefully on adapters that can't delimit — everything just appears as files\n * at the root.\n */\nexport const FileBrowser = ({\n  files,\n  initialPrefix = \"\",\n  delimiter = \"/\",\n  onSelect,\n  onChanged,\n  readOnly = false,\n  className,\n}: FileBrowserProps) => {\n  const [prefix, setPrefix] = useState(initialPrefix);\n  const [folders, setFolders] = useState<string[]>([]);\n  const [items, setItems] = useState<StoredFile[]>([]);\n  const [cursor, setCursor] = useState<string | undefined>();\n  const [isLoading, setIsLoading] = useState(true);\n\n  // Read `files` through a ref so the fetch effect depends only on `prefix` —\n  // the hook returns a fresh object whenever its ambient store changes, so\n  // depending on it directly would re-list on every change (an infinite loop\n  // the moment a `list` errors). Same pattern as `file-list`.\n  const filesRef = useRef(files);\n  filesRef.current = files;\n\n  const load = useCallback(\n    async (next?: string) => {\n      setIsLoading(true);\n      try {\n        const result = await filesRef.current.list({\n          delimiter,\n          prefix: prefix || undefined,\n          ...(next ? { cursor: next } : {}),\n        });\n        setFolders((prev) =>\n          next\n            ? [...new Set([...prev, ...(result.prefixes ?? [])])]\n            : (result.prefixes ?? [])\n        );\n        setItems((prev) => (next ? [...prev, ...result.items] : result.items));\n        setCursor(result.cursor);\n      } catch {\n        // The hook mirrors the error to `files.error` for display; don't re-fetch.\n      } finally {\n        setIsLoading(false);\n      }\n    },\n    [prefix, delimiter]\n  );\n\n  useEffect(() => {\n    void load();\n  }, [load]);\n\n  // A copy/rename/move/delete can move a key out of (or into) the current\n  // folder, so re-list the prefix from scratch rather than splicing locally.\n  const changed = useCallback(() => {\n    void load();\n    onChanged?.();\n  }, [load, onChanged]);\n\n  const crumbs = crumbsOf(prefix, delimiter);\n  const isEmpty = !(isLoading || folders.length || items.length);\n\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)}>\n      <nav className=\"flex flex-wrap items-center gap-0.5 text-sm\">\n        <Button\n          onClick={() => setPrefix(\"\")}\n          size=\"icon-xs\"\n          type=\"button\"\n          variant=\"ghost\"\n        >\n          <HomeIcon />\n        </Button>\n        {crumbs.map((crumb) => (\n          <Fragment key={crumb.prefix}>\n            <ChevronRightIcon className=\"size-3 text-muted-foreground\" />\n            <Button\n              onClick={() => setPrefix(crumb.prefix)}\n              size=\"xs\"\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              {crumb.label}\n            </Button>\n          </Fragment>\n        ))}\n      </nav>\n\n      <ul className=\"flex flex-col gap-1\">\n        {folders.map((folder) => (\n          <li key={folder}>\n            <button\n              className=\"flex w-full items-center gap-3 rounded-lg border border-border p-2 text-left transition-colors hover:bg-muted\"\n              onClick={() => setPrefix(folder)}\n              type=\"button\"\n            >\n              <span className=\"flex size-9 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground\">\n                <FolderIcon className=\"size-4\" />\n              </span>\n              <span className=\"min-w-0 flex-1 truncate font-medium text-sm\">\n                {folderName(folder, prefix, delimiter)}\n              </span>\n              <ChevronRightIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n            </button>\n          </li>\n        ))}\n        {items.map((item) => (\n          <li\n            className=\"flex items-center gap-3 rounded-lg border border-border p-2\"\n            key={item.key}\n          >\n            <button\n              className=\"-m-1 flex min-w-0 flex-1 items-center gap-3 rounded-md p-1 text-left transition-colors hover:bg-muted disabled:cursor-default disabled:hover:bg-transparent\"\n              disabled={!onSelect}\n              onClick={() => onSelect?.(item)}\n              type=\"button\"\n            >\n              <span className=\"flex size-9 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground\">\n                <FileIcon className=\"size-4\" />\n              </span>\n              <span className=\"min-w-0 flex-1\">\n                <span className=\"block truncate font-medium text-sm\">\n                  {folderName(item.key, prefix, delimiter) || item.key}\n                </span>\n                <span className=\"block text-muted-foreground text-xs\">\n                  {formatBytes(item.size)} · {item.type || \"unknown\"}\n                </span>\n              </span>\n            </button>\n            {!readOnly && (\n              <FileActions\n                files={files}\n                fileKey={item.key}\n                onChanged={changed}\n              />\n            )}\n          </li>\n        ))}\n      </ul>\n\n      {isLoading && (\n        <div className=\"flex items-center justify-center gap-2 p-4 text-muted-foreground text-sm\">\n          <Loader2Icon className=\"size-4 animate-spin\" /> Loading…\n        </div>\n      )}\n\n      {isEmpty && (\n        <div className=\"flex flex-col items-center gap-1 p-8 text-center text-muted-foreground text-sm\">\n          <FolderIcon className=\"size-6\" />\n          This folder is empty.\n        </div>\n      )}\n\n      {cursor && !isLoading && (\n        <Button\n          onClick={() => void load(cursor)}\n          size=\"sm\"\n          type=\"button\"\n          variant=\"outline\"\n        >\n          Load more\n        </Button>\n      )}\n    </div>\n  );\n};\n"
    }
  ]
}
