{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload",
  "dependencies": [
    "iconsax-react",
    "sonner"
  ],
  "files": [
    {
      "path": "src/components/watermelon-ui/file-upload.tsx",
      "content": "\"use client\";\n\nimport React, { useState, useRef, useCallback } from \"react\";\nimport {\n  DocumentUpload,\n  CloseCircle,\n  DocumentText,\n  Image as ImageIcon,\n} from \"iconsax-react\";\nimport { toast } from \"sonner\";\n\n// ============================================================================\n// Core Types\n// ============================================================================\n\nexport interface UploadedFile {\n  id: string;\n  file: File;\n  name: string;\n  size: number;\n  type: string;\n  uploadResult?: Record<string, unknown>; // Provider-specific upload result\n  preview?: string;\n}\n\nexport interface UploadResponse {\n  success: boolean;\n  data?: Record<string, unknown>; // Provider-specific data (URL, key, public_id, etc.)\n  error?: string;\n}\n\nexport interface UploadProvider {\n  name: string;\n  uploadFile: (\n    file: File,\n    options?: Record<string, unknown>\n  ) => Promise<UploadResponse>;\n  uploadFiles?: (\n    files: File[],\n    options?: Record<string, unknown>\n  ) => Promise<UploadResponse[]>;\n}\n\nexport interface FileUploadProps {\n  onFilesChange?: (files: UploadedFile[]) => void;\n  provider?: UploadProvider | null; // null = local only\n  uploadOptions?: Record<string, string>; // Provider-specific options\n  acceptedTypes?: string[];\n  maxFileSize?: number; // in MB\n  maxFiles?: number;\n  required?: boolean;\n  title?: string;\n  description?: string;\n  className?: string;\n  multiple?: boolean;\n  showPreviews?: boolean;\n  localMode?: boolean; // Force local-only mode (no upload)\n  onUploadStart?: (file: File) => void;\n  onUploadSuccess?: (file: File, result?: Record<string, unknown>) => void;\n  onUploadError?: (file: File, error: string) => void;\n}\n\n// ============================================================================\n// Built-in Upload Providers\n// ============================================================================\n\nexport class S3Provider implements UploadProvider {\n  name = \"s3\";\n  private baseUrl: string;\n  private apiKey: string;\n\n  constructor(baseUrl: string, apiKey: string) {\n    this.baseUrl = baseUrl;\n    this.apiKey = apiKey;\n  }\n\n  async uploadFile(\n    file: File,\n    options: { folder?: string } = {}\n  ): Promise<UploadResponse> {\n    try {\n      const formData = new FormData();\n      formData.append(\"file\", file);\n      formData.append(\"folder\", options.folder || \"uploads\");\n\n      const response = await fetch(`${this.baseUrl}/api/s3/upload`, {\n        method: \"POST\",\n        headers: {\n          \"x-api-key\": this.apiKey,\n        },\n        body: formData,\n      });\n\n      const result = await response.json();\n      return {\n        success: result.success,\n        data: result.file,\n        error: result.error,\n      };\n    } catch (error) {\n      return {\n        success: false,\n        error: (error as Error).message,\n      };\n    }\n  }\n\n  async uploadFiles(\n    files: File[],\n    options: { folder?: string } = {}\n  ): Promise<UploadResponse[]> {\n    try {\n      const formData = new FormData();\n      files.forEach((file) => formData.append(\"files\", file));\n      formData.append(\"folder\", options.folder || \"uploads\");\n\n      const response = await fetch(`${this.baseUrl}/api/s3/upload-multiple`, {\n        method: \"POST\",\n        headers: {\n          \"x-api-key\": this.apiKey,\n        },\n        body: formData,\n      });\n\n      const result = await response.json();\n      return result.files || [];\n    } catch (error) {\n      return files.map(() => ({\n        success: false,\n        error: (error as Error).message,\n      }));\n    }\n  }\n}\n\nexport class CloudinaryProvider implements UploadProvider {\n  name = \"cloudinary\";\n  private cloudName: string;\n  private apiKey: string;\n\n  constructor(cloudName: string, apiKey: string) {\n    this.cloudName = cloudName;\n    this.apiKey = apiKey;\n  }\n\n  async uploadFile(\n    file: File,\n    options: { preset?: string; resourceType?: string } = {}\n  ): Promise<UploadResponse> {\n    try {\n      const formData = new FormData();\n      formData.append(\"file\", file);\n      formData.append(\"upload_preset\", options.preset || \"default\");\n      formData.append(\"api_key\", this.apiKey);\n\n      const resourceType =\n        options.resourceType ||\n        (file.type.startsWith(\"image/\") ? \"image\" : \"raw\");\n      const response = await fetch(\n        `https://api.cloudinary.com/v1_1/${this.cloudName}/${resourceType}/upload`,\n        {\n          method: \"POST\",\n          body: formData,\n        }\n      );\n\n      const data = await response.json();\n\n      if (data.error) {\n        return {\n          success: false,\n          error: data.error.message,\n        };\n      }\n\n      return {\n        success: true,\n        data: {\n          url: data.secure_url,\n          publicId: data.public_id,\n          resourceType: data.resource_type,\n          format: data.format,\n        },\n      };\n    } catch (error) {\n      return {\n        success: false,\n        error: (error as Error).message,\n      };\n    }\n  }\n}\n\nexport class CustomAPIProvider implements UploadProvider {\n  name = \"custom\";\n  private endpoint: string;\n  private headers: Record<string, string>;\n\n  constructor(endpoint: string, headers: Record<string, string> = {}) {\n    this.endpoint = endpoint;\n    this.headers = headers;\n  }\n\n  async uploadFile(\n    file: File,\n    options: Record<string, unknown> = {}\n  ): Promise<UploadResponse> {\n    try {\n      const formData = new FormData();\n      formData.append(\"file\", file);\n\n      // Add any additional options to form data\n      Object.entries(options).forEach(([key, value]) => {\n        formData.append(key, String(value));\n      });\n\n      const response = await fetch(this.endpoint, {\n        method: \"POST\",\n        headers: this.headers,\n        body: formData,\n      });\n\n      const data = await response.json();\n\n      return {\n        success: response.ok,\n        data,\n        error: response.ok ? undefined : data.message || \"Upload failed\",\n      };\n    } catch (error) {\n      return {\n        success: false,\n        error: (error as Error).message,\n      };\n    }\n  }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nconst formatFileSize = (bytes: number): string => {\n  if (bytes === 0) return \"0 Bytes\";\n  const k = 1024;\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n};\n\nconst getFileIcon = (fileType: string) => {\n  if (fileType.startsWith(\"image/\")) {\n    return (\n      <ImageIcon\n        className=\"h-6 w-6 text-blue-500\"\n        variant=\"Bulk\"\n        color=\"currentColor\"\n      />\n    );\n  }\n  return (\n    <DocumentText\n      className=\"h-6 w-6 text-blue-500\"\n      variant=\"Bulk\"\n      color=\"currentColor\"\n    />\n  );\n};\n\nconst createFilePreview = (file: File): Promise<string | undefined> => {\n  return new Promise((resolve) => {\n    if (file.type.startsWith(\"image/\")) {\n      const reader = new FileReader();\n      reader.onload = (e) => resolve(e.target?.result as string);\n      reader.onerror = () => resolve(undefined);\n      reader.readAsDataURL(file);\n    } else {\n      resolve(undefined);\n    }\n  });\n};\n\n// ============================================================================\n// Main Component\n// ============================================================================\n\nconst FileUpload: React.FC<FileUploadProps> = ({\n  onFilesChange,\n  provider = null,\n  uploadOptions = {},\n  acceptedTypes = [\n    \".pdf\",\n    \".doc\",\n    \".docx\",\n    \".ppt\",\n    \".pptx\",\n    \".jpg\",\n    \".jpeg\",\n    \".png\",\n  ],\n  maxFileSize = 10, // 10MB default\n  maxFiles = 5,\n  required = false,\n  title = \"Upload Files\",\n  description = \"Drag and drop files here or click to browse\",\n  className = \"\",\n  multiple = true,\n  showPreviews = true,\n  localMode = false,\n  onUploadStart,\n  onUploadSuccess,\n  onUploadError,\n}) => {\n  const [isDragOver, setIsDragOver] = useState(false);\n  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);\n  const [isUploading, setIsUploading] = useState(false);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n\n  const validateFile = useCallback((file: File): string | null => {\n    // Check file size\n    if (file.size > maxFileSize * 1024 * 1024) {\n      return `File size must be less than ${maxFileSize}MB`;\n    }\n\n    // Check file type\n    const fileExtension = \".\" + file.name.split(\".\").pop()?.toLowerCase();\n    if (!acceptedTypes.some((type) => type.toLowerCase() === fileExtension)) {\n      return `File type not supported. Accepted types: ${acceptedTypes.join(\n        \", \"\n      )}`;\n    }\n\n    return null;\n  }, [maxFileSize, acceptedTypes]);\n\n  const processFiles = useCallback(async (files: FileList | File[]) => {\n    setIsUploading(true);\n    const fileArray = Array.from(files);\n    const newFiles: UploadedFile[] = [];\n\n    for (const file of fileArray) {\n      // Validate file\n      const error = validateFile(file);\n      if (error) {\n        toast.error(`${file.name}: ${error}`);\n        continue;\n      }\n\n      // Check if we've reached max files\n      if (uploadedFiles.length + newFiles.length >= maxFiles) {\n        toast.warning(`Maximum ${maxFiles} files allowed`);\n        break;\n      }\n\n      try {\n        onUploadStart?.(file);\n\n        // Create preview\n        const preview = await createFilePreview(file);\n\n        // Create base file object\n        const uploadedFile: UploadedFile = {\n          id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n          file,\n          name: file.name,\n          size: file.size,\n          type: file.type,\n          preview,\n        };\n\n        // Upload if provider is available and not in local mode\n        if (provider && !localMode) {\n          const uploadResult = await provider.uploadFile(file, uploadOptions);\n\n          if (uploadResult.success) {\n            uploadedFile.uploadResult = uploadResult.data;\n            toast.success(`${file.name} uploaded successfully`);\n            onUploadSuccess?.(file, uploadResult.data);\n          } else {\n            toast.error(`Upload failed: ${uploadResult.error}`);\n            onUploadError?.(file, uploadResult.error || \"Upload failed\");\n            continue;\n          }\n        } else {\n          // Local mode or no provider\n          toast.success(`${file.name} added successfully`);\n        }\n\n        newFiles.push(uploadedFile);\n      } catch (error) {\n        console.error(\"Failed to process file\", { file, error });\n        toast.error(`Failed to process ${file.name}`);\n        onUploadError?.(file, (error as Error).message);\n      }\n    }\n\n    const updatedFiles = multiple ? [...uploadedFiles, ...newFiles] : newFiles;\n    setUploadedFiles(updatedFiles);\n    onFilesChange?.(updatedFiles);\n    setIsUploading(false);\n  }, [\n    validateFile,\n    uploadedFiles,\n    maxFiles,\n    onUploadStart,\n    provider,\n    localMode,\n    uploadOptions,\n    onUploadSuccess,\n    onUploadError,\n    multiple,\n    onFilesChange,\n  ]);\n\n  const handleDrop = useCallback(\n    (e: React.DragEvent<HTMLDivElement>) => {\n      e.preventDefault();\n      setIsDragOver(false);\n\n      const files = e.dataTransfer.files;\n      if (files.length > 0) {\n        processFiles(files);\n      }\n    },\n    [processFiles]\n  );\n\n  const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {\n    e.preventDefault();\n    setIsDragOver(true);\n  }, []);\n\n  const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {\n    e.preventDefault();\n    setIsDragOver(false);\n  }, []);\n\n  const handleFileSelect = () => {\n    fileInputRef.current?.click();\n  };\n\n  const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const files = e.target.files;\n    if (files && files.length > 0) {\n      processFiles(files);\n    }\n    // Reset input value to allow re-uploading the same file\n    e.target.value = \"\";\n  };\n\n  const removeFile = (fileId: string) => {\n    const updatedFiles = uploadedFiles.filter((file) => file.id !== fileId);\n    setUploadedFiles(updatedFiles);\n    onFilesChange?.(updatedFiles);\n  };\n\n  const clearAllFiles = () => {\n    setUploadedFiles([]);\n    onFilesChange?.([]);\n  };\n\n  return (\n    <div className={`space-y-4 ${className}`}>\n      {/* Upload Area */}\n      <div\n        onDrop={handleDrop}\n        onDragOver={handleDragOver}\n        onDragLeave={handleDragLeave}\n        onClick={handleFileSelect}\n        className={`relative cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${isDragOver\n          ? \"border-blue-400 bg-blue-50 dark:border-blue-500 dark:bg-blue-950/20\"\n          : \"border-zinc-300 hover:border-zinc-400 dark:border-zinc-600 dark:hover:border-zinc-500\"\n          } ${uploadedFiles.length > 0\n            ? \"bg-zinc-50 dark:bg-zinc-800/50\"\n            : \"hover:bg-zinc-50 dark:hover:bg-zinc-800/50\"\n          } `}\n      >\n        <input\n          ref={fileInputRef}\n          type=\"file\"\n          className=\"hidden\"\n          accept={acceptedTypes.join(\",\")}\n          multiple={multiple}\n          onChange={handleFileInputChange}\n        />\n\n        <div className=\"flex flex-col items-center\">\n          <DocumentUpload\n            className={`mx-auto h-12 w-12 ${isDragOver ? \"text-blue-500\" : \"text-zinc-400\"\n              }`}\n            variant=\"Bulk\"\n            color=\"currentColor\"\n          />\n\n          <div className=\"mt-4\">\n            <p\n              className={`text-sm font-medium ${isDragOver\n                ? \"text-blue-600\"\n                : \"text-zinc-700 dark:text-zinc-300\"\n                }`}\n            >\n              {title}\n              {required && <span className=\"ml-1 text-red-500\">*</span>}\n            </p>\n            <p className=\"mt-1 text-xs text-zinc-500 dark:text-zinc-400\">\n              {description}\n            </p>\n            <p className=\"mt-2 text-xs text-zinc-400\">\n              Max {maxFiles} files, {maxFileSize}MB each\n            </p>\n            <p className=\"text-xs text-zinc-400\">\n              Supported: {acceptedTypes.join(\", \")}\n            </p>\n            {(localMode || !provider) && (\n              <p className=\"text-xs text-amber-600 dark:text-amber-400 mt-1\">\n                Local mode - files won&apos;t be uploaded\n              </p>\n            )}\n          </div>\n        </div>\n\n        {isUploading && (\n          <div className=\"absolute inset-0 flex items-center justify-center rounded-lg bg-white/80 dark:bg-zinc-800/80\">\n            <div className=\"flex items-center gap-2\">\n              <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent\"></div>\n              <span className=\"text-sm text-zinc-600\">Processing files...</span>\n            </div>\n          </div>\n        )}\n      </div>\n\n      {/* Uploaded Files List */}\n      {uploadedFiles.length > 0 && (\n        <div className=\"space-y-3\">\n          <div className=\"flex items-center justify-between\">\n            <h4 className=\"text-sm font-medium text-zinc-700 dark:text-zinc-300\">\n              Uploaded Files ({uploadedFiles.length}/{maxFiles})\n            </h4>\n            {uploadedFiles.length > 1 && (\n              <button\n                type=\"button\"\n                onClick={clearAllFiles}\n                className=\"text-xs text-red-600 hover:text-red-700 dark:text-red-400\"\n              >\n                Clear All\n              </button>\n            )}\n          </div>\n\n          <div className=\"grid gap-2\">\n            {uploadedFiles.map((file) => (\n              <div\n                key={file.id}\n                className=\"flex items-center justify-between rounded-lg border border-zinc-200 bg-white p-3 dark:border-zinc-600 dark:bg-zinc-800\"\n              >\n                <div className=\"flex items-center gap-3\">\n                  {showPreviews && file.preview ? (\n\n                    <img\n                      src={file.preview}\n                      alt={file.name}\n                      className=\"h-10 w-10 rounded object-cover\"\n                    />\n                  ) : (\n                    getFileIcon(file.type)\n                  )}\n\n                  <div className=\"min-w-0 flex-1\">\n                    <p className=\"truncate text-sm font-medium text-zinc-700 dark:text-zinc-300\">\n                      {file.name}\n                    </p>\n                    <p className=\"text-xs text-zinc-500\">\n                      {formatFileSize(file.size)}\n                      {file.uploadResult && (\n                        <span className=\"ml-2 text-green-600\">• Uploaded</span>\n                      )}\n                    </p>\n                  </div>\n                </div>\n\n                <button\n                  type=\"button\"\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    removeFile(file.id);\n                  }}\n                  className=\"rounded-full p-1 text-zinc-400 hover:bg-zinc-100 hover:text-red-500 dark:hover:bg-zinc-700\"\n                  title=\"Remove file\"\n                >\n                  <CloseCircle\n                    className=\"h-4 w-4\"\n                    variant=\"Bulk\"\n                    color=\"currentColor\"\n                  />\n                </button>\n              </div>\n            ))}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default FileUpload;\n\n// ============================================================================\n// Usage Examples\n// ============================================================================\n\n/*\n// S3 Upload\nconst s3Provider = new S3Provider(\n  process.env.NEXT_PUBLIC_MAIL_NOTIFIER_URL!,\n  process.env.NEXT_PUBLIC_MAIL_NOTIFIER_KEY!\n);\n\n<FileUpload\n  onFilesChange={(files) => console.log(files)}\n  provider={s3Provider}\n  uploadOptions={{ folder: \"documents\" }}\n/>\n\n// Cloudinary Upload\nconst cloudinaryProvider = new CloudinaryProvider(\n  process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME!,\n  process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY!\n);\n\n<FileUpload\n  onFilesChange={(files) => console.log(files)}\n  provider={cloudinaryProvider}\n  uploadOptions={{ preset: \"my_preset\" }}\n/>\n\n// Custom API Upload\nconst customProvider = new CustomAPIProvider(\n  \"https://my-api.com/upload\",\n  { \"Authorization\": \"Bearer token\" }\n);\n\n<FileUpload\n  onFilesChange={(files) => console.log(files)}\n  provider={customProvider}\n  uploadOptions={{ category: \"documents\" }}\n/>\n\n// Local Only (no upload)\n<FileUpload\n  onFilesChange={(files) => console.log(files)}\n  localMode={true}\n/>\n\n// Custom Provider\nconst myCustomProvider: UploadProvider = {\n  name: \"my-service\",\n  async uploadFile(file: File, options?: any) {\n    // Your custom upload logic here\n    return {\n      success: true,\n      data: { url: \"https://example.com/file.pdf\" }\n    };\n  }\n};\n\n<FileUpload\n  onFilesChange={(files) => console.log(files)}\n  provider={myCustomProvider}\n  onUploadSuccess={(file, result) => console.log(\"Uploaded:\", file.name, result)}\n  onUploadError={(file, error) => console.error(\"Failed:\", file.name, error)}\n/>\n*/\n",
      "type": "registry:ui",
      "target": "components/ui/file-upload.tsx"
    }
  ],
  "type": "registry:ui"
}
