{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-actions",
  "type": "registry:component",
  "title": "File Actions",
  "description": "Per-file actions menu — download, copy, rename, move and delete.",
  "dependencies": [
    "files-sdk",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dialog",
    "dropdown-menu",
    "input"
  ],
  "files": [
    {
      "path": "registry/files-sdk/file-actions/file-actions.tsx",
      "type": "registry:component",
      "target": "components/files-sdk/file-actions.tsx",
      "content": "\"use client\";\n\nimport type { UseFilesResult } from \"files-sdk/react\";\nimport {\n  CopyIcon,\n  DownloadIcon,\n  Loader2Icon,\n  MoreHorizontalIcon,\n  PencilIcon,\n  Trash2Icon,\n} from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useState } from \"react\";\n\nimport { Button, buttonVariants } 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  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface FileActionsProps {\n  /** A `useFiles()` instance — every action runs through it. */\n  files: UseFilesResult;\n  /** The key the actions operate on. */\n  fileKey: string;\n  /** Called after a successful copy/rename/move/delete so the parent can refresh. */\n  onChanged?: () => void;\n  /** Custom trigger content, rendered inside the trigger button. Defaults to a styled `⋯` icon. */\n  children?: ReactNode;\n  className?: string;\n}\n\ntype Action = \"copy\" | \"rename\" | \"move\" | \"delete\";\n\nconst TITLES: Record<Action, string> = {\n  copy: \"Copy to\",\n  delete: \"Delete file\",\n  move: \"Move to\",\n  rename: \"Rename file\",\n};\n\nconst parentOf = (key: string): string => {\n  const slash = key.lastIndexOf(\"/\");\n  return slash === -1 ? \"\" : key.slice(0, slash + 1);\n};\n\n/**\n * A `⋯` actions menu for a single key — download, copy, rename, move and delete,\n * all routed through the `useFiles()` instance you pass in. Copy/rename/move open\n * a small prompt for the destination; delete confirms first.\n */\nexport const FileActions = ({\n  files,\n  fileKey,\n  onChanged,\n  children,\n  className,\n}: FileActionsProps) => {\n  const [action, setAction] = useState<Action | null>(null);\n  const [dest, setDest] = useState(\"\");\n  const [busy, setBusy] = useState(false);\n\n  const open = useCallback(\n    (next: Action) => {\n      const parent = parentOf(fileKey);\n      // Rename edits just the basename; copy/move edit the whole key.\n      setDest(next === \"rename\" ? fileKey.slice(parent.length) : fileKey);\n      setAction(next);\n    },\n    [fileKey]\n  );\n\n  const download = useCallback(async () => {\n    const file = await files.download(fileKey);\n    const blob = await file.blob();\n    const url = URL.createObjectURL(blob);\n    const anchor = document.createElement(\"a\");\n    anchor.download = fileKey.split(\"/\").pop() ?? fileKey;\n    anchor.href = url;\n    anchor.click();\n    URL.revokeObjectURL(url);\n  }, [files, fileKey]);\n\n  const confirm = useCallback(async () => {\n    if (!action) {\n      return;\n    }\n    setBusy(true);\n    try {\n      if (action === \"delete\") {\n        await files.delete(fileKey);\n      } else if (action === \"copy\") {\n        await files.copy(fileKey, dest);\n      } else {\n        // rename + move are both a `move`; rename re-attaches the parent prefix.\n        const target = action === \"rename\" ? parentOf(fileKey) + dest : dest;\n        await files.move(fileKey, target);\n      }\n      setAction(null);\n      onChanged?.();\n    } catch {\n      // The hook mirrors the error to `files.error` for display.\n    } finally {\n      setBusy(false);\n    }\n  }, [action, dest, files, fileKey, onChanged]);\n\n  const isRename = action === \"rename\";\n  const isDelete = action === \"delete\";\n  const destUnchanged =\n    action === \"rename\"\n      ? fileKey.slice(parentOf(fileKey).length) === dest\n      : fileKey === dest;\n\n  return (\n    <>\n      <DropdownMenu>\n        {/* Styled via buttonVariants instead of asChild-wrapping a Button: the\n            trigger must work with both the Radix and Base UI shadcn flavors,\n            and Base UI has no asChild (nesting a Button renders <button> inside\n            <button>). */}\n        <DropdownMenuTrigger\n          className={cn(\n            !children && buttonVariants({ size: \"icon-sm\", variant: \"ghost\" }),\n            className\n          )}\n        >\n          {children ?? (\n            <>\n              <MoreHorizontalIcon />\n              <span className=\"sr-only\">Actions</span>\n            </>\n          )}\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\">\n          {/* onClick rather than Radix's onSelect: Base UI menu items have no\n              onSelect prop, and Radix items fire a real click on both pointer\n              and keyboard selection, so onClick works with both flavors. */}\n          <DropdownMenuItem onClick={() => void download()}>\n            <DownloadIcon />\n            Download\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => open(\"copy\")}>\n            <CopyIcon />\n            Copy\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => open(\"rename\")}>\n            <PencilIcon />\n            Rename\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => open(\"move\")}>\n            <CopyIcon />\n            Move\n          </DropdownMenuItem>\n          <DropdownMenuSeparator />\n          <DropdownMenuItem\n            onClick={() => open(\"delete\")}\n            variant=\"destructive\"\n          >\n            <Trash2Icon />\n            Delete\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n\n      <Dialog\n        onOpenChange={(next) => !next && setAction(null)}\n        open={action !== null}\n      >\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{action ? TITLES[action] : \"\"}</DialogTitle>\n            <DialogDescription className=\"truncate\">\n              {fileKey}\n            </DialogDescription>\n          </DialogHeader>\n\n          {!isDelete && (\n            <Input\n              autoFocus\n              onChange={(event) => setDest(event.target.value)}\n              placeholder={isRename ? \"New name\" : \"Destination key\"}\n              value={dest}\n            />\n          )}\n\n          <DialogFooter>\n            <Button\n              onClick={() => setAction(null)}\n              type=\"button\"\n              variant=\"outline\"\n            >\n              Cancel\n            </Button>\n            <Button\n              disabled={busy || (!isDelete && (!dest.trim() || destUnchanged))}\n              onClick={() => void confirm()}\n              type=\"button\"\n              variant={isDelete ? \"destructive\" : \"default\"}\n            >\n              {busy && <Loader2Icon className=\"animate-spin\" />}\n              {isDelete ? \"Delete\" : \"Confirm\"}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n};\n"
    }
  ]
}
