{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-manager",
  "title": "File Manager",
  "description": "A file manager block with upload, list/grid layout, and item actions.",
  "dependencies": [
    "@dnd-kit/core",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "button",
    "dialog",
    "dropdown-menu",
    "https://ui.corr.sh/r/file-upload-dropzone.json"
  ],
  "files": [
    {
      "path": "registry/corr/components/file-manager.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  DndContext,\n  PointerSensor,\n  useDraggable,\n  useDroppable,\n  useSensor,\n  useSensors,\n  type DragEndEvent,\n} from \"@dnd-kit/core\"\nimport {\n  ArrowLeft,\n  Download,\n  ChevronDown,\n  FileText,\n  Folder,\n  FolderOpen,\n  Grid2X2,\n  List,\n  MoreHorizontal,\n  Rows3,\n  Trash2,\n  UploadCloud,\n} from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  FileUploadDropzone,\n  type FileUploadDropzoneFile,\n} from \"./file-upload-dropzone\"\n\nexport type FileManagerView = \"details\" | \"list\" | \"cards\"\nexport type FileManagerMobileMode = \"auto\" | \"list\" | \"sheet\"\n\nexport type FileManagerItem = {\n  id: string\n  name: string\n  size?: string\n  modified?: string\n  type?: string\n  path?: string\n  kind?: \"file\" | \"folder\"\n}\n\nfunction normalizePath(path: string) {\n  if (!path || path === \"/\") return \"/\"\n  return `/${path.split(\"/\").filter(Boolean).join(\"/\")}`\n}\n\nfunction joinPath(base: string, name: string) {\n  return normalizePath(`${base}/${name}`)\n}\n\nfunction getPathParts(path: string) {\n  return normalizePath(path).split(\"/\").filter(Boolean)\n}\n\nfunction getParentPath(path: string) {\n  const parts = getPathParts(path)\n  return normalizePath(parts.slice(0, -1).join(\"/\"))\n}\n\nfunction getItemPath(item: FileManagerItem) {\n  return normalizePath(item.path ?? \"/\")\n}\n\nfunction getFolderPath(item: FileManagerItem) {\n  return item.kind === \"folder\"\n    ? joinPath(getItemPath(item), item.name)\n    : getItemPath(item)\n}\n\nfunction getDerivedFolders(files: FileManagerItem[]) {\n  const folders = new Set<string>([\"/\"])\n\n  files.forEach((file) => {\n    const path = getItemPath(file)\n    const parts = getPathParts(path)\n\n    parts.forEach((_, index) => {\n      folders.add(normalizePath(parts.slice(0, index + 1).join(\"/\")))\n    })\n\n    if (file.kind === \"folder\") {\n      folders.add(getFolderPath(file))\n    }\n  })\n\n  return Array.from(folders).sort((left, right) => {\n    const depth = getPathParts(left).length - getPathParts(right).length\n    return depth === 0 ? left.localeCompare(right) : depth\n  })\n}\n\nfunction getNavigationDirection(currentPath: string, nextPath: string) {\n  const current = normalizePath(currentPath)\n  const next = normalizePath(nextPath)\n\n  if (next === current) return 0\n  if (next.startsWith(`${current}/`)) return 1\n  if (current.startsWith(`${next}/`)) return -1\n\n  return getPathParts(next).length >= getPathParts(current).length ? 1 : -1\n}\n\nexport function FileManager({\n  files,\n  path,\n  defaultPath = \"/\",\n  defaultView = \"details\",\n  onPathChange,\n  onUpload,\n  onDelete,\n  onOpen,\n  onMove,\n  mobileMode = \"auto\",\n  className,\n}: {\n  files: FileManagerItem[]\n  path?: string\n  defaultPath?: string\n  defaultView?: FileManagerView\n  mobileMode?: FileManagerMobileMode\n  onPathChange?: (path: string) => void\n  onUpload?: (path: string, files: FileUploadDropzoneFile[]) => void\n  onDelete?: (file: FileManagerItem) => void\n  onOpen?: (file: FileManagerItem) => void\n  onMove?: (file: FileManagerItem, targetPath: string) => void\n  className?: string\n}) {\n  const [internalPath, setInternalPath] = React.useState(defaultPath)\n  const [view, setView] = React.useState<FileManagerView>(defaultView)\n  const [transitionDirection, setTransitionDirection] = React.useState(1)\n  const [uploadOpen, setUploadOpen] = React.useState(false)\n  const [folderPickerOpen, setFolderPickerOpen] = React.useState(false)\n  const [uploadFiles, setUploadFiles] = React.useState<\n    FileUploadDropzoneFile[]\n  >([])\n  const reduceMotion = useReducedMotion()\n  const sensors = useSensors(\n    useSensor(PointerSensor, {\n      activationConstraint: {\n        delay: 160,\n        tolerance: 6,\n      },\n    })\n  )\n  const currentPath = normalizePath(path ?? internalPath)\n  const forceMobileLayout = mobileMode === \"list\" || mobileMode === \"sheet\"\n  const compactFolderNavigation = mobileMode === \"sheet\"\n  const folders = React.useMemo(() => getDerivedFolders(files), [files])\n  const visibleFiles = React.useMemo(\n    () => files.filter((file) => getItemPath(file) === currentPath),\n    [currentPath, files]\n  )\n  const visibleView =\n    forceMobileLayout && view === \"details\" ? \"list\" : view\n  const viewOptions = [\n    { value: \"details\", icon: Rows3, label: \"Details\" },\n    { value: \"list\", icon: List, label: \"List\" },\n    { value: \"cards\", icon: Grid2X2, label: \"Cards\" },\n  ].filter((item) => !forceMobileLayout || item.value !== \"details\")\n\n  function setPath(nextPath: string) {\n    const normalized = normalizePath(nextPath)\n    setTransitionDirection(getNavigationDirection(currentPath, normalized))\n    setInternalPath(normalized)\n    onPathChange?.(normalized)\n  }\n\n  function openItem(file: FileManagerItem) {\n    if (file.kind === \"folder\") {\n      setPath(getFolderPath(file))\n      return\n    }\n    onOpen?.(file)\n  }\n\n  const isUploading = uploadFiles.some((file) => file.status === \"uploading\")\n  const uploadableFiles = uploadFiles.filter((file) => file.status !== \"error\")\n\n  function submitUpload() {\n    if (uploadableFiles.length === 0 || isUploading) return\n    onUpload?.(currentPath, uploadableFiles)\n    setUploadOpen(false)\n    setUploadFiles([])\n  }\n\n  function handleDragEnd(event: DragEndEvent) {\n    const activeId = String(event.active.id)\n    const overId = event.over ? String(event.over.id) : null\n\n    if (!overId?.startsWith(\"folder:\")) return\n\n    const item = files.find((file) => file.id === activeId)\n    if (!item) return\n\n    const targetPath = normalizePath(overId.replace(\"folder:\", \"\"))\n    const currentItemPath = getItemPath(item)\n\n    if (targetPath === currentItemPath) return\n\n    if (item.kind === \"folder\") {\n      const sourceFolderPath = getFolderPath(item)\n      if (\n        targetPath === sourceFolderPath ||\n        targetPath.startsWith(`${sourceFolderPath}/`)\n      ) {\n        return\n      }\n    }\n\n    onMove?.(item, targetPath)\n  }\n\n  const mobileFolderList = (\n    <div className=\"space-y-1\">\n      {folders.map((folder) => {\n        const active = folder === currentPath\n        const label = folder === \"/\" ? \"Files\" : getPathParts(folder).at(-1)\n        const depth = getPathParts(folder).length\n\n        return (\n          <FileManagerTreeFolder\n            key={folder}\n            folder={folder}\n            label={label ?? \"Folder\"}\n            depth={depth}\n            active={active}\n            onClick={() => {\n              setPath(folder)\n              setFolderPickerOpen(false)\n            }}\n          />\n        )\n      })}\n    </div>\n  )\n\n  return (\n    <DndContext sensors={sensors} onDragEnd={handleDragEnd}>\n      <div\n        className={cn(\"overflow-hidden rounded-md border bg-card\", className)}\n      >\n        <div className=\"border-b p-3\">\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <div className=\"flex min-w-0 items-center gap-2\">\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                disabled={currentPath === \"/\"}\n                onClick={() => setPath(getParentPath(currentPath))}\n              >\n                <ArrowLeft />\n                <span className=\"sr-only\">Go back</span>\n              </Button>\n              {compactFolderNavigation ? (\n                <FileManagerPathMenu\n                  folders={folders}\n                  currentPath={currentPath}\n                  onSelect={setPath}\n                />\n              ) : (\n                <div className=\"flex min-w-0 flex-wrap items-center gap-1 text-sm font-medium\">\n                  <button\n                    type=\"button\"\n                    className=\"rounded px-1 py-0.5 hover:bg-muted\"\n                    onClick={() => setPath(\"/\")}\n                  >\n                    Files\n                  </button>\n                  {getPathParts(currentPath).map((part, index, parts) => (\n                    <React.Fragment key={`${part}-${index}`}>\n                      <span className=\"text-muted-foreground\">/</span>\n                      <button\n                        type=\"button\"\n                        className=\"rounded px-1 py-0.5 hover:bg-muted\"\n                        onClick={() =>\n                          setPath(parts.slice(0, index + 1).join(\"/\"))\n                        }\n                      >\n                        {part}\n                      </button>\n                    </React.Fragment>\n                  ))}\n                </div>\n              )}\n            </div>\n            <div className=\"flex items-center gap-2\">\n              {mobileMode === \"list\" || compactFolderNavigation ? null : (\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"sm\"\n                  className={cn(!forceMobileLayout && \"md:hidden\")}\n                  onClick={() => setFolderPickerOpen(true)}\n                >\n                  <FolderOpen />\n                  Folders\n                </Button>\n              )}\n              <div className=\"flex rounded-md border bg-background p-0.5\">\n                {viewOptions.map((item) => {\n                  const Icon = item.icon\n\n                  return (\n                    <Button\n                      key={item.value}\n                      variant={\n                        visibleView === item.value ? \"secondary\" : \"ghost\"\n                      }\n                      size=\"icon-sm\"\n                      onClick={() => setView(item.value as FileManagerView)}\n                    >\n                      <Icon />\n                      <span className=\"sr-only\">{item.label}</span>\n                    </Button>\n                  )\n                })}\n              </div>\n              <Button size=\"sm\" onClick={() => setUploadOpen(true)}>\n                <UploadCloud />\n                Upload\n              </Button>\n            </div>\n          </div>\n          <div className=\"mt-2 pl-10 text-xs text-muted-foreground\">\n            {visibleFiles.length}{\" \"}\n            {visibleFiles.length === 1 ? \"item\" : \"items\"}\n            {compactFolderNavigation ? null : \" in this folder\"}\n          </div>\n        </div>\n\n        {mobileMode === \"list\" ? (\n          <div className=\"border-b p-2 md:hidden\">\n            <div className=\"scrollbar-none flex gap-1 overflow-x-auto\">\n              {folders.map((folder) => {\n                const active = folder === currentPath\n                const label =\n                  folder === \"/\" ? \"Files\" : getPathParts(folder).at(-1)\n\n                return (\n                  <Button\n                    key={folder}\n                    type=\"button\"\n                    variant={active ? \"secondary\" : \"ghost\"}\n                    size=\"sm\"\n                    className=\"shrink-0\"\n                    onClick={() => setPath(folder)}\n                  >\n                    {active ? <FolderOpen /> : <Folder />}\n                    {label}\n                  </Button>\n                )\n              })}\n            </div>\n          </div>\n        ) : null}\n\n        <div\n          className={cn(\n            \"grid min-h-80\",\n            !forceMobileLayout && \"md:grid-cols-[13rem_minmax(0,1fr)]\"\n          )}\n        >\n          <aside\n            className={cn(\n              \"hidden border-b bg-muted/20 p-2 md:block md:border-r md:border-b-0\",\n              forceMobileLayout && \"md:hidden\"\n            )}\n          >\n            <div className=\"space-y-1\">\n              {folders.map((folder) => {\n                const active = folder === currentPath\n                const label =\n                  folder === \"/\" ? \"Files\" : getPathParts(folder).at(-1)\n                const depth = getPathParts(folder).length\n\n                return (\n                  <FileManagerTreeFolder\n                    key={folder}\n                    folder={folder}\n                    label={label ?? \"Folder\"}\n                    depth={depth}\n                    active={active}\n                    onClick={() => setPath(folder)}\n                  />\n                )\n              })}\n            </div>\n          </aside>\n\n          <main className={cn(\"min-w-0 p-3\", forceMobileLayout && \"p-2\")}>\n            <AnimatePresence mode=\"wait\" initial={false}>\n              <motion.div\n                key={`${currentPath}-${visibleView}`}\n                initial={{\n                  opacity: 0,\n                  x: reduceMotion ? 0 : transitionDirection * 12,\n                }}\n                animate={{ opacity: 1, x: 0 }}\n                exit={{\n                  opacity: 0,\n                  x: reduceMotion ? 0 : transitionDirection * -12,\n                }}\n                transition={{ duration: reduceMotion ? 0 : 0.16 }}\n              >\n                {visibleFiles.length === 0 ? (\n                  <div className=\"rounded-md border border-dashed px-4 py-10 text-center text-sm text-muted-foreground\">\n                    This folder is empty.\n                  </div>\n                ) : visibleView === \"details\" ? (\n                  <div className=\"overflow-hidden rounded-md border\">\n                    <div className=\"grid grid-cols-[minmax(0,1fr)_7rem_8rem_2rem] gap-3 border-b bg-muted/30 px-3 py-2 text-xs font-medium text-muted-foreground\">\n                      <div>Name</div>\n                      <div>Size</div>\n                      <div>Modified</div>\n                      <div />\n                    </div>\n                    {visibleFiles.map((file) => (\n                      <FileManagerRow\n                        key={file.id}\n                        file={file}\n                        view=\"details\"\n                        onOpen={openItem}\n                        onDelete={onDelete}\n                        draggable={Boolean(onMove)}\n                      />\n                    ))}\n                  </div>\n                ) : (\n                  <div\n                    className={cn(\n                      \"grid gap-2\",\n                      visibleView === \"cards\" &&\n                        (forceMobileLayout\n                          ? \"grid-cols-1\"\n                          : \"sm:grid-cols-2 lg:grid-cols-3\")\n                    )}\n                  >\n                    {visibleFiles.map((file) => (\n                      <FileManagerRow\n                        key={file.id}\n                        file={file}\n                        view={visibleView}\n                        onOpen={openItem}\n                        onDelete={onDelete}\n                        draggable={Boolean(onMove)}\n                      />\n                    ))}\n                  </div>\n                )}\n              </motion.div>\n            </AnimatePresence>\n          </main>\n        </div>\n\n        <Dialog open={folderPickerOpen} onOpenChange={setFolderPickerOpen}>\n          <DialogContent className=\"sm:max-w-sm\">\n            <DialogHeader>\n              <DialogTitle>Choose folder</DialogTitle>\n              <DialogDescription>\n                Move through the file tree on smaller screens.\n              </DialogDescription>\n            </DialogHeader>\n            {mobileFolderList}\n          </DialogContent>\n        </Dialog>\n\n        <Dialog\n          open={uploadOpen}\n          onOpenChange={(open) => {\n            setUploadOpen(open)\n            if (!open) setUploadFiles([])\n          }}\n        >\n          <DialogContent className=\"sm:max-w-md\">\n            <DialogHeader>\n              <DialogTitle>Upload files</DialogTitle>\n              <DialogDescription>\n                Add files to {currentPath === \"/\" ? \"Files\" : currentPath}.\n              </DialogDescription>\n            </DialogHeader>\n            <FileUploadDropzone\n              files={uploadFiles}\n              onFilesChange={setUploadFiles}\n              simulateUpload\n              label=\"Drop files to upload\"\n              description=\"Uploads are added to the active directory\"\n            />\n            <DialogFooter>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                onClick={() => setUploadOpen(false)}\n              >\n                Cancel\n              </Button>\n              <Button\n                type=\"button\"\n                onClick={submitUpload}\n                disabled={uploadableFiles.length === 0 || isUploading}\n              >\n                Upload files\n              </Button>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n      </div>\n    </DndContext>\n  )\n}\n\nfunction FileManagerPathMenu({\n  folders,\n  currentPath,\n  onSelect,\n}: {\n  folders: string[]\n  currentPath: string\n  onSelect: (path: string) => void\n}) {\n  const parts = getPathParts(currentPath)\n  const currentLabel =\n    currentPath === \"/\" ? \"Files\" : parts.at(-1) ?? \"Current folder\"\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"sm\"\n          className=\"h-9 max-w-56 justify-start gap-2 px-2\"\n        >\n          <FolderOpen className=\"size-4 shrink-0\" />\n          <span className=\"min-w-0 truncate text-sm font-medium\">\n            {currentLabel}\n          </span>\n          <ChevronDown className=\"ml-auto size-3.5 shrink-0 text-muted-foreground\" />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\" className=\"w-64\">\n        <DropdownMenuLabel>Path</DropdownMenuLabel>\n        <DropdownMenuItem onClick={() => onSelect(\"/\")}>\n          <Folder className=\"size-4\" />\n          Files\n        </DropdownMenuItem>\n        {parts.map((part, index) => {\n          const path = normalizePath(parts.slice(0, index + 1).join(\"/\"))\n\n          return (\n            <DropdownMenuItem key={`${part}-${path}`} onClick={() => onSelect(path)}>\n              <FolderOpen className=\"size-4\" />\n              {part}\n            </DropdownMenuItem>\n          )\n        })}\n        <DropdownMenuSeparator />\n        <DropdownMenuLabel>Jump to folder</DropdownMenuLabel>\n        <div className=\"max-h-56 overflow-y-auto\">\n          {folders.map((folder) => {\n            const active = folder === currentPath\n            const label = folder === \"/\" ? \"Files\" : getPathParts(folder).at(-1)\n            const depth = getPathParts(folder).length\n\n            return (\n              <DropdownMenuItem\n                key={folder}\n                onClick={() => onSelect(folder)}\n                className={cn(active && \"bg-muted font-medium\")}\n                style={{ paddingLeft: `${0.5 + depth * 0.75}rem` }}\n              >\n                {active ? (\n                  <FolderOpen className=\"size-4\" />\n                ) : (\n                  <Folder className=\"size-4\" />\n                )}\n                <span className=\"truncate\">{label}</span>\n              </DropdownMenuItem>\n            )\n          })}\n        </div>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n\nfunction FileManagerTreeFolder({\n  folder,\n  label,\n  depth,\n  active,\n  onClick,\n}: {\n  folder: string\n  label: string\n  depth: number\n  active: boolean\n  onClick: () => void\n}) {\n  const { isOver, setNodeRef } = useDroppable({\n    id: `folder:${folder}`,\n    data: { folder },\n  })\n\n  return (\n    <button\n      ref={setNodeRef}\n      type=\"button\"\n      onClick={onClick}\n      className={cn(\n        \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors hover:bg-muted\",\n        active && \"bg-background font-medium shadow-xs\",\n        isOver && \"bg-primary/10 text-primary ring-1 ring-primary/20\"\n      )}\n      style={{ paddingLeft: `${0.5 + depth * 0.75}rem` }}\n    >\n      {active ? (\n        <FolderOpen className=\"size-3.5 text-muted-foreground\" />\n      ) : (\n        <Folder className=\"size-3.5 text-muted-foreground\" />\n      )}\n      <span className=\"truncate\">{label}</span>\n    </button>\n  )\n}\n\nfunction FileManagerRow({\n  file,\n  view,\n  onOpen,\n  onDelete,\n  draggable = false,\n}: {\n  file: FileManagerItem\n  view: FileManagerView\n  onOpen: (file: FileManagerItem) => void\n  onDelete?: (file: FileManagerItem) => void\n  draggable?: boolean\n}) {\n  const isFolder = file.kind === \"folder\"\n  const Icon = isFolder ? Folder : FileText\n  const { attributes, isDragging, listeners, setNodeRef } = useDraggable({\n    id: file.id,\n    data: { file },\n    disabled: !draggable,\n  })\n\n  if (view === \"details\") {\n    return (\n      <div\n        ref={setNodeRef}\n        data-dragging={isDragging}\n        className=\"grid grid-cols-[minmax(0,1fr)_7rem_8rem_2rem] items-center gap-3 border-b px-3 py-2 opacity-100 last:border-b-0 data-[dragging=true]:opacity-50\"\n        {...attributes}\n        {...listeners}\n      >\n        <button\n          type=\"button\"\n          onClick={() => onOpen(file)}\n          className=\"flex min-w-0 items-center gap-2 text-left\"\n        >\n          <Icon className=\"size-4 shrink-0 text-muted-foreground\" />\n          <span className=\"truncate text-sm font-medium\">{file.name}</span>\n        </button>\n        <div className=\"text-xs text-muted-foreground\">\n          {isFolder ? \"--\" : file.size}\n        </div>\n        <div className=\"text-xs text-muted-foreground\">{file.modified}</div>\n        <FileManagerActions file={file} onDelete={onDelete} />\n      </div>\n    )\n  }\n\n  return (\n    <div\n      ref={setNodeRef}\n      data-dragging={isDragging}\n      className={cn(\n        \"flex items-center gap-3 rounded-md border bg-background p-3\",\n        view === \"cards\" && \"items-start\",\n        isDragging && \"opacity-50\"\n      )}\n      {...attributes}\n      {...listeners}\n    >\n      <button\n        type=\"button\"\n        onClick={() => onOpen(file)}\n        className=\"flex min-w-0 flex-1 items-center gap-3 text-left\"\n      >\n        <div className=\"flex size-9 shrink-0 items-center justify-center rounded-md border bg-muted/40\">\n          <Icon className=\"size-4 text-muted-foreground\" />\n        </div>\n        <div className=\"min-w-0\">\n          <div className=\"truncate text-sm font-medium\">{file.name}</div>\n          <div className=\"mt-1 text-xs text-muted-foreground\">\n            {isFolder ? \"Folder\" : file.size} · {file.modified}\n          </div>\n        </div>\n      </button>\n      <FileManagerActions file={file} onDelete={onDelete} />\n    </div>\n  )\n}\n\nfunction FileManagerActions({\n  file,\n  onDelete,\n}: {\n  file: FileManagerItem\n  onDelete?: (file: FileManagerItem) => void\n}) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon-sm\">\n          <MoreHorizontal />\n          <span className=\"sr-only\">Open file actions</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem>\n          <Download />\n          Download\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          variant=\"destructive\"\n          onClick={() => onDelete?.(file)}\n        >\n          <Trash2 />\n          Delete\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}