{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-file-upload-4","type":"registry:block","title":"Gallery file upload","description":"Gallery file upload","dependencies":["cn"],"registryDependencies":["@reui/alert","button","dialog","spinner","@reui/use-file-upload"],"files":[{"path":"c-file-upload-4.tsx","type":"registry:block","content":"\"use client\"\n\nimport { 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 { cn } from \"cn\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ninterface GalleryUploadProps {\n  maxFiles?: number\n  maxSize?: number\n  accept?: string\n  multiple?: boolean\n  className?: string\n  onFilesChange?: (files: FileWithPreview[]) => void\n}\n\nexport function Pattern({\n  maxFiles = 10,\n  maxSize = 5 * 1024 * 1024, // 5MB\n  accept = \"image/*\",\n  multiple = true,\n  className,\n  onFilesChange,\n}: GalleryUploadProps) {\n  const [selectedImage, setSelectedImage] = useState<string | null>(null)\n  const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>(\n    {}\n  )\n  const [isPreviewLoading, setIsPreviewLoading] = useState(false)\n\n  // Create default images using FileMetadata type\n  const defaultImages: FileMetadata[] = [\n    {\n      id: \"default-1\",\n      name: \"avatar-1.png\",\n      size: 44608,\n      type: \"image/png\",\n      url: \"https://picsum.photos/1000/800?random=1\",\n    },\n    {\n      id: \"default-2\",\n      name: \"avatar-2.png\",\n      size: 42144,\n      type: \"image/png\",\n      url: \"https://picsum.photos/1000/800?random=2\",\n    },\n    {\n      id: \"default-3\",\n      name: \"avatar-2.png\",\n      size: 42144,\n      type: \"image/png\",\n      url: \"https://picsum.photos/1000/800?random=3\",\n    },\n  ]\n\n  const [\n    { files, 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: defaultImages,\n    onFilesChange,\n  })\n\n  const isImage = (file: File | FileMetadata) => {\n    const type = file instanceof File ? file.type : file.type\n    return type.startsWith(\"image/\")\n  }\n\n  return (\n    <div className={cn(\"w-full max-w-4xl\", className)}>\n      {/* Upload Area */}\n      <div\n        className={cn(\n          \"relative rounded-lg border border-dashed p-8 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              \"flex h-16 w-16 items-center justify-center rounded-full\",\n              isDragging ? \"bg-primary/10\" : \"bg-muted\"\n            )}\n          >\n            <IconPlaceholder\n              lucide=\"ImageIcon\"\n              tabler=\"IconPhoto\"\n              hugeicons=\"ImageIcon\"\n              phosphor=\"ImageIcon\"\n              remixicon=\"RiImageLine\"\n              className={cn(\n                \"h-5 w-5\",\n                isDragging ? \"text-primary\" : \"text-muted-foreground\"\n              )}\n            />\n          </div>\n\n          <div className=\"space-y-2\">\n            <h3 className=\"text-lg font-semibold\">Upload images to gallery</h3>\n            <p className=\"text-muted-foreground text-sm\">\n              Drag and drop images here or click to browse\n            </p>\n            <p className=\"text-muted-foreground text-xs\">\n              PNG, JPG, GIF up to {formatBytes(maxSize)} each (max {maxFiles}{\" \"}\n              files)\n            </p>\n          </div>\n\n          <Button onClick={openFileDialog}>\n            <IconPlaceholder\n              lucide=\"UploadIcon\"\n              tabler=\"IconUpload\"\n              hugeicons=\"Upload01Icon\"\n              phosphor=\"UploadSimple\"\n              remixicon=\"RiUpload2Line\"\n              className=\"h-4 w-4\"\n            />\n            Select images\n          </Button>\n        </div>\n      </div>\n\n      {/* Gallery Stats */}\n      {files.length > 0 && (\n        <div className=\"mt-6 flex items-center justify-between\">\n          <div className=\"flex items-center gap-4\">\n            <h4 className=\"text-sm font-medium\">\n              Gallery ({files.length}/{maxFiles})\n            </h4>\n            <div className=\"text-muted-foreground text-xs\">\n              Total:{\" \"}\n              {formatBytes(\n                files.reduce((acc, file) => acc + file.file.size, 0)\n              )}\n            </div>\n          </div>\n          <Button onClick={clearFiles} variant=\"outline\" size=\"sm\">\n            Clear all\n          </Button>\n        </div>\n      )}\n\n      {/* Image Grid */}\n      {files.length > 0 && (\n        <div className=\"mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4\">\n          {files.map((fileItem) => (\n            <div\n              key={fileItem.id}\n              className=\"group/item relative aspect-square\"\n            >\n              {isImage(fileItem.file) && fileItem.preview ? (\n                <>\n                  {loadingImages[fileItem.id] !== false && (\n                    <div className=\"bg-muted/50 absolute inset-0 flex items-center justify-center rounded-lg border\">\n                      <Spinner className=\"text-muted-foreground size-6\" />\n                    </div>\n                  )}\n                  <img\n                    src={fileItem.preview}\n                    alt={fileItem.file.name}\n                    onLoad={() =>\n                      setLoadingImages((prev) => ({\n                        ...prev,\n                        [fileItem.id]: false,\n                      }))\n                    }\n                    className={cn(\n                      \"h-full w-full rounded-lg border object-cover transition-all group-hover/item:scale-105\",\n                      loadingImages[fileItem.id] !== false\n                        ? \"opacity-0\"\n                        : \"opacity-100\"\n                    )}\n                  />\n                </>\n              ) : (\n                <div className=\"bg-muted flex h-full w-full items-center justify-center rounded-lg border\">\n                  <IconPlaceholder\n                    lucide=\"ImageIcon\"\n                    tabler=\"IconPhoto\"\n                    hugeicons=\"ImageIcon\"\n                    phosphor=\"ImageIcon\"\n                    remixicon=\"RiImageLine\"\n                    className=\"text-muted-foreground h-8 w-8\"\n                  />\n                </div>\n              )}\n\n              {/* Overlay */}\n              <div className=\"absolute inset-0 flex items-center justify-center gap-2 bg-black/50 opacity-0 transition-opacity group-hover/item:opacity-100\">\n                {/* View Button */}\n                {fileItem.preview && (\n                  <Button\n                    onClick={() => {\n                      setSelectedImage(fileItem.preview!)\n                      setIsPreviewLoading(true)\n                    }}\n                    variant=\"secondary\"\n                    size=\"icon\"\n                    className=\"size-7\"\n                  >\n                    <IconPlaceholder\n                      lucide=\"ZoomInIcon\"\n                      tabler=\"IconZoomIn\"\n                      hugeicons=\"SearchAddIcon\"\n                      phosphor=\"MagnifyingGlassPlusIcon\"\n                      remixicon=\"RiZoomInLine\"\n                    />\n                  </Button>\n                )}\n\n                {/* Remove Button */}\n                <Button\n                  onClick={() => removeFile(fileItem.id)}\n                  variant=\"secondary\"\n                  size=\"icon\"\n                  className=\"size-7\"\n                >\n                  <IconPlaceholder\n                    lucide=\"XIcon\"\n                    tabler=\"IconX\"\n                    hugeicons=\"MultiplicationSignIcon\"\n                    phosphor=\"XIcon\"\n                    remixicon=\"RiCloseLine\"\n                  />\n                </Button>\n              </div>\n\n              {/* File Info */}\n              <div className=\"absolute right-0 bottom-0 left-0 rounded-b-lg bg-black/70 p-2 text-white opacity-0 transition-opacity group-hover:opacity-100\">\n                <p className=\"truncate text-xs font-medium\">\n                  {fileItem.file.name}\n                </p>\n                <p className=\"text-xs text-gray-300\">\n                  {formatBytes(fileItem.file.size)}\n                </p>\n              </div>\n            </div>\n          ))}\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\n      {/* Image Preview Dialog */}\n      <Dialog\n        open={!!selectedImage}\n        onOpenChange={(open) => !open && setSelectedImage(null)}\n      >\n        <DialogContent className=\"[&_[data-slot=dialog-close]]:text-muted-foreground [&_[data-slot=dialog-close]]:hover:text-foreground [&_[data-slot=dialog-close]]:bg-background w-full border-none bg-transparent p-0 shadow-none sm:max-w-xl [&_[data-slot=dialog-close]]:-end-7 [&_[data-slot=dialog-close]]:-top-7 [&_[data-slot=dialog-close]]:size-7 [&_[data-slot=dialog-close]]:rounded-full\">\n          <DialogHeader className=\"sr-only\">\n            <DialogTitle>Image Preview</DialogTitle>\n          </DialogHeader>\n          <div className=\"flex items-center justify-center\">\n            {selectedImage && (\n              <>\n                {isPreviewLoading && (\n                  <div className=\"absolute inset-0 flex items-center justify-center\">\n                    <Spinner className=\"size-8 text-white\" />\n                  </div>\n                )}\n                <img\n                  src={selectedImage}\n                  alt=\"Preview\"\n                  onLoad={() => setIsPreviewLoading(false)}\n                  className={cn(\n                    \"h-full w-auto rounded-lg object-contain transition-opacity duration-300\",\n                    isPreviewLoading ? \"opacity-0\" : \"opacity-100\"\n                  )}\n                />\n              </>\n            )}\n          </div>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}","target":"components/examples/c-file-upload-4.tsx"}],"meta":{"order":4,"gridSize":1,"label":"Gallery"}}