{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "trash-bin",
  "type": "registry:component",
  "title": "Trash Bin",
  "description": "Lists, restores and permanently purges soft-deleted files (softDelete plugin).",
  "dependencies": [
    "files-sdk",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dialog"
  ],
  "files": [
    {
      "path": "registry/files-sdk/trash-bin/trash-bin.tsx",
      "type": "registry:component",
      "target": "components/files-sdk/trash-bin.tsx",
      "content": "\"use client\";\n\nimport type { TrashedFile, UseFilesResult } from \"files-sdk/react\";\nimport {\n  Loader2Icon,\n  RotateCcwIcon,\n  Trash2Icon,\n  TrashIcon,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useRef, useState } from \"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 { cn } from \"@/lib/utils\";\n\nexport interface TrashBinProps {\n  /** A `useFiles()` instance backed by a gateway with the `softDelete()` plugin. */\n  files: UseFilesResult;\n  /** Called after a successful restore or purge. */\n  onChanged?: () => void;\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/**\n * A recycle bin backed by the `softDelete()` plugin. Lists trashed objects and\n * restores or permanently purges them through the same `useFiles()` instance.\n * Purges (single + \"empty trash\") confirm first, since they're the only way the\n * bytes actually leave storage.\n */\nexport const TrashBin = ({ files, onChanged, className }: TrashBinProps) => {\n  const [trashed, setTrashed] = useState<TrashedFile[]>([]);\n  const [isLoading, setIsLoading] = useState(true);\n  const [busy, setBusy] = useState<string>();\n  // `null` = closed; `{ key }` = purge one; `{}` = empty the whole trash.\n  const [pending, setPending] = useState<{ key?: string } | null>(null);\n\n  const filesRef = useRef(files);\n  filesRef.current = files;\n\n  const refresh = useCallback(async () => {\n    setIsLoading(true);\n    try {\n      setTrashed(await filesRef.current.trashed());\n    } catch {\n      // The hook mirrors the error to `files.error` for display.\n    } finally {\n      setIsLoading(false);\n    }\n  }, []);\n\n  useEffect(() => {\n    void refresh();\n  }, [refresh]);\n\n  const restore = useCallback(\n    async (key: string) => {\n      setBusy(key);\n      try {\n        await filesRef.current.restoreTrashed(key);\n        await refresh();\n        onChanged?.();\n      } catch {\n        // Mirrored to `files.error`.\n      } finally {\n        setBusy(undefined);\n      }\n    },\n    [refresh, onChanged]\n  );\n\n  const purge = useCallback(async () => {\n    if (!pending) {\n      return;\n    }\n    setBusy(pending.key ?? \"all\");\n    try {\n      await filesRef.current.purge(pending.key);\n      setPending(null);\n      await refresh();\n      onChanged?.();\n    } catch {\n      // Mirrored to `files.error`.\n    } finally {\n      setBusy(undefined);\n    }\n  }, [pending, refresh, onChanged]);\n\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)}>\n      {trashed.length > 0 && (\n        <div className=\"flex items-center justify-between\">\n          <p className=\"text-muted-foreground text-xs\">\n            {trashed.length} in trash\n          </p>\n          <Button\n            onClick={() => setPending({})}\n            size=\"sm\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            <TrashIcon />\n            Empty trash\n          </Button>\n        </div>\n      )}\n\n      {isLoading && !trashed.length && (\n        <div className=\"text-muted-foreground flex items-center justify-center gap-2 p-8 text-sm\">\n          <Loader2Icon className=\"size-4 animate-spin\" /> Loading…\n        </div>\n      )}\n\n      {!(isLoading || trashed.length) && (\n        <div className=\"text-muted-foreground flex flex-col items-center gap-1 p-8 text-center text-sm\">\n          <TrashIcon className=\"size-6\" />\n          Trash is empty.\n        </div>\n      )}\n\n      <ul className=\"flex flex-col gap-2\">\n        {trashed.map((item) => (\n          <li\n            className=\"border-border flex items-center gap-3 rounded-lg border p-2\"\n            key={item.key}\n          >\n            <span className=\"bg-muted text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded\">\n              <Trash2Icon className=\"size-4\" />\n            </span>\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"truncate text-sm font-medium\">{item.key}</p>\n              <p className=\"text-muted-foreground text-xs\">\n                {formatBytes(item.size)}\n                {item.lastModified\n                  ? ` · ${new Date(item.lastModified).toLocaleDateString()}`\n                  : \"\"}\n              </p>\n            </div>\n            <Button\n              disabled={busy !== undefined}\n              onClick={() => void restore(item.key)}\n              size=\"icon-sm\"\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              {busy === item.key ? (\n                <Loader2Icon className=\"animate-spin\" />\n              ) : (\n                <RotateCcwIcon />\n              )}\n              <span className=\"sr-only\">Restore</span>\n            </Button>\n            <Button\n              disabled={busy !== undefined}\n              onClick={() => setPending({ key: item.key })}\n              size=\"icon-sm\"\n              type=\"button\"\n              variant=\"destructive\"\n            >\n              <Trash2Icon />\n              <span className=\"sr-only\">Delete forever</span>\n            </Button>\n          </li>\n        ))}\n      </ul>\n\n      <Dialog\n        onOpenChange={(open) => !open && setPending(null)}\n        open={pending !== null}\n      >\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>\n              {pending?.key ? \"Delete forever?\" : \"Empty the trash?\"}\n            </DialogTitle>\n            <DialogDescription>\n              {pending?.key\n                ? `\"${pending.key}\" will be permanently deleted. This can't be undone.`\n                : \"Every item in the trash will be permanently deleted. This can't be undone.\"}\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button\n              onClick={() => setPending(null)}\n              type=\"button\"\n              variant=\"outline\"\n            >\n              Cancel\n            </Button>\n            <Button\n              disabled={busy !== undefined}\n              onClick={() => void purge()}\n              type=\"button\"\n              variant=\"destructive\"\n            >\n              {busy !== undefined && <Loader2Icon className=\"animate-spin\" />}\n              Delete forever\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n};\n"
    }
  ]
}
