{"name":"file-upload","title":"File Upload","description":"Drag and drop file upload with preview.","type":"registry:ui","docs":"/components/file-upload","categories":["forms","data-entry"],"registryDependencies":["https://pb-ui-five.vercel.app/registry/button","https://pb-ui-five.vercel.app/registry/input","https://pb-ui-five.vercel.app/registry/label"],"files":[{"path":"components/ui/file-upload.tsx","target":"components/ui/file-upload.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { FileText, Loader2, Trash2, Upload, X } from \"lucide-react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { Label } from \"./label\";\n\nexport interface FileUploadProps {\n  /** Current file URL (for edit mode) */\n  currentFileUrl?: string | null;\n  /** Callback when a file is selected */\n  onFileSelect: (file: File | null) => void;\n  /** Callback when user wants to remove existing file */\n  onRemoveExisting?: () => void;\n  /** Whether the upload is in progress */\n  isUploading?: boolean;\n  /** Optional error message */\n  error?: string;\n  /** Label for the field */\n  label?: string;\n  /** Description for the field */\n  description?: string;\n  /** Whether the field is disabled */\n  disabled?: boolean;\n  /** Whether the field is required */\n  required?: boolean;\n  showError?: boolean;\n  className?: string;\n  validateFile?: (file: File) => {\n    valid: boolean;\n    error?: string | undefined;\n  };\n  clickToUploadText?: string;\n  orDragAndDropText?: string;\n  fileTypesText?: string;\n  invalidFileErrorText?: string;\n  uploadingText?: string;\n  viewCurrentFileText?: string;\n  replaceText?: string;\n}\n\nexport function FileUpload({\n  currentFileUrl,\n  onFileSelect,\n  onRemoveExisting,\n  isUploading = false,\n  error,\n  label = \"File Upload\",\n  description = \"Upload your file here.\",\n  disabled = false,\n  required = false,\n  className,\n  validateFile,\n  clickToUploadText = \"Click to upload\",\n  orDragAndDropText = \"or drag and drop\",\n  fileTypesText = \"PDF, DOC, DOCX (max 10MB)\",\n  invalidFileErrorText = \"Invalid file\",\n  uploadingText = \"Uploading...\",\n  viewCurrentFileText = \"View current file\",\n  replaceText = \"Replace\",\n  showError = true,\n}: FileUploadProps) {\n  const [selectedFile, setSelectedFile] = useState<File | null>(null);\n  const [isDragging, setIsDragging] = useState(false);\n  const [validationError, setValidationError] = useState<string | null>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const handleFileChange = useCallback(\n    (file: File | null) => {\n      if (validateFile && file) {\n        const validation = validateFile(file);\n        if (!validation.valid) {\n          setValidationError(validation.error || invalidFileErrorText);\n          return;\n        }\n      }\n      setValidationError(null);\n      setSelectedFile(file);\n      onFileSelect(file);\n    },\n    [onFileSelect, validateFile, invalidFileErrorText],\n  );\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0] || null;\n    handleFileChange(file);\n  };\n\n  const handleDrop = useCallback(\n    (e: React.DragEvent) => {\n      e.preventDefault();\n      setIsDragging(false);\n\n      if (disabled || isUploading) return;\n\n      const file = e.dataTransfer.files?.[0] || null;\n      handleFileChange(file);\n    },\n    [disabled, isUploading, handleFileChange],\n  );\n\n  const handleDragOver = (e: React.DragEvent) => {\n    e.preventDefault();\n    if (!disabled && !isUploading) {\n      setIsDragging(true);\n    }\n  };\n\n  const handleDragLeave = (e: React.DragEvent) => {\n    e.preventDefault();\n    setIsDragging(false);\n  };\n\n  const handleRemoveSelected = () => {\n    setSelectedFile(null);\n    setValidationError(null);\n    onFileSelect(null);\n    if (inputRef.current) {\n      inputRef.current.value = \"\";\n    }\n  };\n\n  const handleRemoveExisting = () => {\n    onRemoveExisting?.();\n    handleRemoveSelected();\n  };\n\n  const handleClick = () => {\n    if (!disabled && !isUploading) {\n      inputRef.current?.click();\n    }\n  };\n\n  const displayError = validationError || error;\n  const hasExistingFile = currentFileUrl && !selectedFile;\n  const hasSelectedFile = selectedFile !== null;\n\n  // Extract filename from URL for display\n  const existingFileName = currentFileUrl\n    ? decodeURIComponent(currentFileUrl.split(\"/\").pop() || \"curriculum\")\n    : null;\n\n  return (\n    <div className={cn(\"space-y-4\", className)}>\n      {label && (\n        <Label>\n          {label}\n          {required && (\n            <span aria-hidden className=\"ps-1 text-destructive\">\n              *\n            </span>\n          )}\n        </Label>\n      )}\n      <div\n        className={cn(\n          \"bg-muted/50 shadow-sm border-2 border-transparent border-dashed rounded-lg transition-all\",\n          isDragging && \"border-primary bg-primary/5\",\n          displayError && \"border-destructive\",\n          disabled && \"opacity-50 cursor-not-allowed\",\n          !disabled &&\n            !isUploading &&\n            \"cursor-pointer hover:bg-muted/80 hover:border-ring/30\",\n        )}\n        onDrop={handleDrop}\n        onDragOver={handleDragOver}\n        onDragLeave={handleDragLeave}\n        onClick={handleClick}\n      >\n        <Input\n          ref={inputRef}\n          type=\"file\"\n          accept=\".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n          onChange={handleInputChange}\n          disabled={disabled || isUploading}\n          className=\"hidden\"\n        />\n\n        {isUploading ? (\n          <div className=\"flex flex-col justify-center items-center py-4\">\n            <Loader2 className=\"mb-2 w-8 h-8 text-primary animate-spin\" />\n            <p className=\"text-muted-foreground text-sm\">{uploadingText}</p>\n          </div>\n        ) : hasSelectedFile ? (\n          <div className=\"flex justify-between items-center\">\n            <div className=\"flex items-center gap-3\">\n              <FileText className=\"w-8 h-8 text-primary\" />\n              <div>\n                <p className=\"font-medium text-sm\">{selectedFile.name}</p>\n                <p className=\"text-muted-foreground text-xs\">\n                  {(selectedFile.size / 1024 / 1024).toFixed(2)} MB\n                </p>\n              </div>\n            </div>\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"sm\"\n              onClick={(e) => {\n                e.stopPropagation();\n                handleRemoveSelected();\n              }}\n              disabled={disabled}\n            >\n              <X className=\"size-4\" />\n            </Button>\n          </div>\n        ) : hasExistingFile ? (\n          <div className=\"flex justify-between items-center\">\n            <div className=\"flex items-center gap-3\">\n              <FileText className=\"w-8 h-8 text-primary\" />\n              <div>\n                <p className=\"font-medium text-sm\">{existingFileName}</p>\n                <a\n                  href={currentFileUrl}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"text-primary text-xs hover:underline\"\n                  onClick={(e) => e.stopPropagation()}\n                >\n                  {viewCurrentFileText}\n                </a>\n              </div>\n            </div>\n            <div className=\"flex gap-2\">\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={(e) => {\n                  e.stopPropagation();\n                  handleClick();\n                }}\n                disabled={disabled}\n              >\n                <Upload className=\"mr-1 size-4\" />\n                {replaceText}\n              </Button>\n              {onRemoveExisting && (\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    handleRemoveExisting();\n                  }}\n                  disabled={disabled}\n                  className=\"text-destructive hover:text-destructive\"\n                >\n                  <Trash2 className=\"size-4\" />\n                </Button>\n              )}\n            </div>\n          </div>\n        ) : (\n          <div className=\"flex flex-col justify-center items-center py-4\">\n            <Upload className=\"mb-2 w-8 h-8 text-muted-foreground\" />\n            <p className=\"text-muted-foreground text-sm text-center\">\n              <span className=\"font-medium text-primary\">\n                {clickToUploadText}\n              </span>{\" \"}\n              {orDragAndDropText}\n            </p>\n            <p className=\"mt-1 text-muted-foreground text-xs\">\n              {fileTypesText}\n            </p>\n          </div>\n        )}\n      </div>\n\n      {description && !displayError && (\n        <p className=\"text-muted-foreground text-sm\">{description}</p>\n      )}\n\n      {showError && displayError && (\n        <p className=\"text-destructive text-sm\">{displayError}</p>\n      )}\n    </div>\n  );\n}\n"}]}