{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-file-upload-6","type":"registry:block","title":"Table file upload","description":"Table file upload","dependencies":["cn"],"registryDependencies":["@reui/alert","@reui/badge","button","table","@reui/use-file-upload"],"files":[{"path":"c-file-upload-6.tsx","type":"registry:block","content":"\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport {\n  formatBytes,\n  useFileUpload,\n  type FileMetadata,\n  type FileWithPreview,\n} from \"@/hooks/use-file-upload\"\nimport {\n  Alert,\n  AlertDescription,\n  AlertTitle,\n} from \"@/components/reui/alert\"\nimport { Badge } from \"@/components/reui/badge\"\nimport { cn } from \"cn\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ninterface FileUploadItem extends FileWithPreview {\n  progress: number\n  status: \"uploading\" | \"completed\" | \"error\"\n  error?: string\n}\n\ninterface TableUploadProps {\n  maxFiles?: number\n  maxSize?: number\n  accept?: string\n  multiple?: boolean\n  className?: string\n  onFilesChange?: (files: FileWithPreview[]) => void\n  simulateUpload?: boolean\n}\n\nexport function Pattern({\n  maxFiles = 10,\n  maxSize = 50 * 1024 * 1024, // 50MB\n  accept = \"*\",\n  multiple = true,\n  className,\n  onFilesChange,\n  simulateUpload = true,\n}: TableUploadProps) {\n  // Create default files using FileMetadata type\n  const defaultFiles: FileMetadata[] = [\n    {\n      id: \"default-doc-1\",\n      name: \"document.pdf\",\n      size: 529254,\n      type: \"application/pdf\",\n      url: \"/media/files/document.pdf\",\n    },\n    {\n      id: \"default-doc-2\",\n      name: \"intro.zip\",\n      size: 252846,\n      type: \"application/zip\",\n      url: \"/media/files/intro.zip\",\n    },\n    {\n      id: \"default-doc-3\",\n      name: \"conclusion.xlsx\",\n      size: 353126,\n      type: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n      url: \"/media/files/conclusion.xlsx\",\n    },\n    {\n      id: \"default-doc-4\",\n      name: \"package.json\",\n      size: 697,\n      type: \"application/json\",\n      url: \"/media/files/package.json\",\n    },\n  ]\n\n  // Convert default files to FileUploadItem format\n  const defaultUploadFiles: FileUploadItem[] = defaultFiles.map((file) => ({\n    id: file.id,\n    file: {\n      name: file.name,\n      size: file.size,\n      type: file.type,\n    } as File,\n    preview: file.url,\n    progress: 100,\n    status: \"completed\" as const,\n  }))\n\n  const [uploadFiles, setUploadFiles] =\n    useState<FileUploadItem[]>(defaultUploadFiles)\n\n  const [\n    { isDragging, errors },\n    {\n      removeFile,\n      clearFiles,\n      handleDragEnter,\n      handleDragLeave,\n      handleDragOver,\n      handleDrop,\n      openFileDialog,\n      getInputProps,\n    },\n  ] = useFileUpload({\n    maxFiles,\n    maxSize,\n    accept,\n    multiple,\n    initialFiles: defaultFiles,\n    onFilesChange: (newFiles) => {\n      // Convert to upload items when files change, preserving existing status\n      const newUploadFiles = newFiles.map((file) => {\n        // Check if this file already exists in uploadFiles\n        const existingFile = uploadFiles.find(\n          (existing) => existing.id === file.id\n        )\n\n        if (existingFile) {\n          // Preserve existing file status and progress\n          return {\n            ...existingFile,\n            ...file, // Update any changed properties from the file\n          }\n        } else {\n          // New file - set to uploading\n          return {\n            ...file,\n            progress: 0,\n            status: \"uploading\" as const,\n          }\n        }\n      })\n      setUploadFiles(newUploadFiles)\n      onFilesChange?.(newFiles)\n    },\n  })\n\n  // Simulate upload progress\n  useEffect(() => {\n    if (!simulateUpload) return\n\n    const interval = setInterval(() => {\n      setUploadFiles((prev) =>\n        prev.map((file) => {\n          if (file.status !== \"uploading\") return file\n\n          const increment = Math.random() * 15 + 5 // 5-20% increment\n          const newProgress = Math.min(file.progress + increment, 100)\n\n          if (newProgress >= 100) {\n            // Randomly decide if upload succeeds or fails\n            const shouldFail = Math.random() < 0.1 // 10% chance to fail\n            return {\n              ...file,\n              progress: 100,\n              status: shouldFail ? (\"error\" as const) : (\"completed\" as const),\n              error: shouldFail\n                ? \"Upload failed. Please try again.\"\n                : undefined,\n            }\n          }\n\n          return { ...file, progress: newProgress }\n        })\n      )\n    }, 500)\n\n    return () => clearInterval(interval)\n  }, [simulateUpload])\n\n  const removeUploadFile = (fileId: string) => {\n    setUploadFiles((prev) => prev.filter((file) => file.id !== fileId))\n    removeFile(fileId)\n  }\n\n  const retryUpload = (fileId: string) => {\n    setUploadFiles((prev) =>\n      prev.map((file) =>\n        file.id === fileId\n          ? {\n              ...file,\n              progress: 0,\n              status: \"uploading\" as const,\n              error: undefined,\n            }\n          : file\n      )\n    )\n  }\n\n  const getFileIcon = (file: File | FileMetadata) => {\n    const type = file instanceof File ? file.type : file.type\n    if (type.startsWith(\"image/\"))\n      return (\n        <IconPlaceholder\n          lucide=\"ImageIcon\"\n          tabler=\"IconPhoto\"\n          hugeicons=\"ImageIcon\"\n          phosphor=\"ImageIcon\"\n          remixicon=\"RiImageLine\"\n          className=\"size-4\"\n        />\n      )\n    if (type.startsWith(\"video/\"))\n      return (\n        <IconPlaceholder\n          lucide=\"VideoIcon\"\n          tabler=\"IconVideo\"\n          hugeicons=\"Video02Icon\"\n          phosphor=\"VideoCameraIcon\"\n          remixicon=\"RiVideoOnLine\"\n          className=\"size-4\"\n        />\n      )\n    if (type.startsWith(\"audio/\"))\n      return (\n        <IconPlaceholder\n          lucide=\"HeadphonesIcon\"\n          tabler=\"IconHeadphones\"\n          hugeicons=\"HeadphonesIcon\"\n          phosphor=\"Headphones\"\n          remixicon=\"RiCustomerServiceLine\"\n          className=\"size-4\"\n        />\n      )\n    if (type.includes(\"pdf\"))\n      return (\n        <IconPlaceholder\n          lucide=\"FileTextIcon\"\n          tabler=\"IconFileText\"\n          hugeicons=\"File02Icon\"\n          phosphor=\"FileTextIcon\"\n          remixicon=\"RiFileTextLine\"\n          className=\"size-4\"\n        />\n      )\n    if (type.includes(\"word\") || type.includes(\"doc\"))\n      return (\n        <IconPlaceholder\n          lucide=\"FileTextIcon\"\n          tabler=\"IconFileText\"\n          hugeicons=\"File02Icon\"\n          phosphor=\"FileTextIcon\"\n          remixicon=\"RiFileTextLine\"\n          className=\"size-4\"\n        />\n      )\n    if (type.includes(\"excel\") || type.includes(\"sheet\"))\n      return (\n        <IconPlaceholder\n          lucide=\"FileSpreadsheetIcon\"\n          tabler=\"IconFileSpreadsheet\"\n          hugeicons=\"GoogleSheetIcon\"\n          phosphor=\"FileTextIcon\"\n          remixicon=\"RiFileTextLine\"\n          className=\"size-4\"\n        />\n      )\n    if (type.includes(\"zip\") || type.includes(\"rar\"))\n      return (\n        <IconPlaceholder\n          lucide=\"FileArchiveIcon\"\n          tabler=\"IconFileZip\"\n          hugeicons=\"ArchiveIcon\"\n          phosphor=\"FileArchive\"\n          remixicon=\" RiFileZipLine\"\n          className=\"size-4\"\n        />\n      )\n    return (\n      <IconPlaceholder\n        lucide=\"FileTextIcon\"\n        tabler=\"IconFileText\"\n        hugeicons=\"File02Icon\"\n        phosphor=\"FileTextIcon\"\n        remixicon=\"RiFileTextLine\"\n        className=\"size-4\"\n      />\n    )\n  }\n\n  const getFileTypeLabel = (file: File | FileMetadata) => {\n    const type = file instanceof File ? file.type : file.type\n    if (type.startsWith(\"image/\")) return \"Image\"\n    if (type.startsWith(\"video/\")) return \"Video\"\n    if (type.startsWith(\"audio/\")) return \"Audio\"\n    if (type.includes(\"pdf\")) return \"PDF\"\n    if (type.includes(\"word\") || type.includes(\"doc\")) return \"Word\"\n    if (type.includes(\"excel\") || type.includes(\"sheet\")) return \"Excel\"\n    if (type.includes(\"zip\") || type.includes(\"rar\")) return \"Archive\"\n    if (type.includes(\"json\")) return \"JSON\"\n    if (type.includes(\"text\")) return \"Text\"\n    return \"File\"\n  }\n\n  return (\n    <div className={cn(\"w-full space-y-4\", className)}>\n      {/* Upload Area */}\n      <div\n        className={cn(\n          \"relative rounded-lg border border-dashed p-6 text-center transition-colors\",\n          isDragging\n            ? \"border-primary bg-primary/5\"\n            : \"border-muted-foreground/25 hover:border-muted-foreground/50\"\n        )}\n        onDragEnter={handleDragEnter}\n        onDragLeave={handleDragLeave}\n        onDragOver={handleDragOver}\n        onDrop={handleDrop}\n      >\n        <input {...getInputProps()} className=\"sr-only\" />\n\n        <div className=\"flex flex-col items-center gap-4\">\n          <div\n            className={cn(\n              \"bg-muted flex h-12 w-12 items-center justify-center rounded-full transition-colors\",\n              isDragging\n                ? \"border-primary bg-primary/10\"\n                : \"border-muted-foreground/25\"\n            )}\n          >\n            <IconPlaceholder\n              lucide=\"UploadIcon\"\n              tabler=\"IconUpload\"\n              hugeicons=\"Upload01Icon\"\n              phosphor=\"UploadSimple\"\n              remixicon=\"RiUpload2Line\"\n              className=\"text-muted-foreground h-5 w-5\"\n            />\n          </div>\n\n          <div className=\"space-y-2\">\n            <p className=\"text-sm font-medium\">\n              Drop files here or{\" \"}\n              <button\n                type=\"button\"\n                onClick={openFileDialog}\n                className=\"text-primary cursor-pointer underline-offset-4 hover:underline\"\n              >\n                browse files\n              </button>\n            </p>\n            <p className=\"text-muted-foreground text-xs\">\n              Maximum file size: {formatBytes(maxSize)} • Maximum files:{\" \"}\n              {maxFiles}\n            </p>\n          </div>\n        </div>\n      </div>\n\n      {/* Files Table */}\n      {uploadFiles.length > 0 && (\n        <div className=\"space-y-4\">\n          <div className=\"flex items-center justify-between\">\n            <h3 className=\"text-sm font-medium\">\n              Files ({uploadFiles.length})\n            </h3>\n            <div className=\"flex gap-2\">\n              <Button onClick={openFileDialog} variant=\"outline\" size=\"sm\">\n                <IconPlaceholder\n                  lucide=\"CloudUploadIcon\"\n                  tabler=\"IconCloudUpload\"\n                  hugeicons=\"CloudUploadIcon\"\n                  phosphor=\"CloudArrowUpIcon\"\n                  remixicon=\"RiUploadCloud2Line\"\n                  className=\"h-4 w-4\"\n                />\n                Add files\n              </Button>\n              <Button onClick={clearFiles} variant=\"outline\" size=\"sm\">\n                <IconPlaceholder\n                  lucide=\"Trash2Icon\"\n                  tabler=\"IconTrash\"\n                  hugeicons=\"Delete02Icon\"\n                  phosphor=\"TrashIcon\"\n                  remixicon=\"RiDeleteBinLine \"\n                  className=\"h-4 w-4\"\n                />\n                Remove all\n              </Button>\n            </div>\n          </div>\n\n          <div className=\"rounded-lg border\">\n            <Table>\n              <TableHeader>\n                <TableRow className=\"text-xs\">\n                  <TableHead className=\"h-9 ps-4\">Name</TableHead>\n                  <TableHead className=\"h-9\">Type</TableHead>\n                  <TableHead className=\"h-9\">Size</TableHead>\n                  <TableHead className=\"h-9 w-[100px] ps-4\">Actions</TableHead>\n                </TableRow>\n              </TableHeader>\n              <TableBody>\n                {uploadFiles.map((fileItem) => (\n                  <TableRow key={fileItem.id}>\n                    <TableCell className=\"py-2 ps-1.5\">\n                      <div className=\"flex items-center gap-1\">\n                        <div\n                          className={cn(\n                            \"text-muted-foreground/80 relative flex size-8 shrink-0 items-center justify-center\"\n                          )}\n                        >\n                          {fileItem.status === \"uploading\" ? (\n                            <div className=\"relative\">\n                              {/* Circular progress background */}\n                              <svg\n                                className=\"size-8 -rotate-90\"\n                                viewBox=\"0 0 32 32\"\n                              >\n                                <circle\n                                  cx=\"16\"\n                                  cy=\"16\"\n                                  r=\"14\"\n                                  fill=\"none\"\n                                  stroke=\"currentColor\"\n                                  strokeWidth=\"2\"\n                                  className=\"text-muted-foreground/20\"\n                                />\n                                {/* Progress circle */}\n                                <circle\n                                  cx=\"16\"\n                                  cy=\"16\"\n                                  r=\"14\"\n                                  fill=\"none\"\n                                  stroke=\"currentColor\"\n                                  strokeWidth=\"2\"\n                                  strokeDasharray={`${2 * Math.PI * 14}`}\n                                  strokeDashoffset={`${2 * Math.PI * 14 * (1 - fileItem.progress / 100)}`}\n                                  className=\"text-primary transition-all duration-300\"\n                                  strokeLinecap=\"round\"\n                                />\n                              </svg>\n                              {/* File icon in center */}\n                              <div className=\"absolute inset-0 flex items-center justify-center\">\n                                {getFileIcon(fileItem.file)}\n                              </div>\n                            </div>\n                          ) : (\n                            <div className=\"flex size-8 items-center justify-center\">\n                              {getFileIcon(fileItem.file)}\n                            </div>\n                          )}\n                        </div>\n                        <p className=\"flex items-center gap-1 truncate text-sm font-medium\">\n                          {fileItem.file.name}\n                          {fileItem.status === \"error\" && (\n                            <Badge variant=\"destructive-light\" size=\"sm\">\n                              Error\n                            </Badge>\n                          )}\n                        </p>\n                      </div>\n                    </TableCell>\n                    <TableCell className=\"py-2\">\n                      <Badge variant=\"secondary\" className=\"text-xs\">\n                        {getFileTypeLabel(fileItem.file)}\n                      </Badge>\n                    </TableCell>\n                    <TableCell className=\"text-muted-foreground py-2 text-sm\">\n                      {formatBytes(fileItem.file.size)}\n                    </TableCell>\n                    <TableCell className=\"py-2\">\n                      <div className=\"flex items-center gap-1\">\n                        {fileItem.preview && (\n                          <Button\n                            size=\"icon\"\n                            variant=\"ghost\"\n                            className=\"size-8\"\n                          >\n                            <IconPlaceholder\n                              lucide=\"DownloadIcon\"\n                              tabler=\"IconDownload\"\n                              hugeicons=\"Download01Icon\"\n                              phosphor=\"DownloadSimpleIcon\"\n                              remixicon=\"RiDownload2Line\"\n                              className=\"size-3.5\"\n                            />\n                          </Button>\n                        )}\n                        {fileItem.status === \"error\" ? (\n                          <Button\n                            onClick={() => retryUpload(fileItem.id)}\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            className=\"text-destructive/80 hover:text-destructive size-8\"\n                          >\n                            <IconPlaceholder\n                              lucide=\"RefreshCwIcon\"\n                              tabler=\"IconRefreshDot\"\n                              hugeicons=\"Refresh04Icon\"\n                              phosphor=\"ArrowsClockwiseIcon\"\n                              remixicon=\"RiRestartLine\"\n                              className=\"size-3.5\"\n                            />\n                          </Button>\n                        ) : (\n                          <Button\n                            onClick={() => removeUploadFile(fileItem.id)}\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            className=\"size-8\"\n                          >\n                            <IconPlaceholder\n                              lucide=\"Trash2Icon\"\n                              tabler=\"IconTrash\"\n                              hugeicons=\"Delete02Icon\"\n                              phosphor=\"TrashIcon\"\n                              remixicon=\"RiDeleteBinLine \"\n                              className=\"size-3.5\"\n                            />\n                          </Button>\n                        )}\n                      </div>\n                    </TableCell>\n                  </TableRow>\n                ))}\n              </TableBody>\n            </Table>\n          </div>\n        </div>\n      )}\n\n      {/* Error Messages */}\n      {errors.length > 0 && (\n        <Alert variant=\"destructive\" className=\"mt-5\">\n          <IconPlaceholder\n            lucide=\"CircleAlertIcon\"\n            tabler=\"IconAlertCircle\"\n            hugeicons=\"AlertCircleIcon\"\n            phosphor=\"WarningCircleIcon\"\n            remixicon=\"RiErrorWarningLine\"\n          />\n          <AlertTitle>File upload error(s)</AlertTitle>\n          <AlertDescription>\n            {errors.map((error, index) => (\n              <p key={index} className=\"last:mb-0\">\n                {error}\n              </p>\n            ))}\n          </AlertDescription>\n        </Alert>\n      )}\n    </div>\n  )\n}","target":"components/examples/c-file-upload-6.tsx"}],"meta":{"order":6,"gridSize":1,"label":"Table"}}