{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upload-file",
  "type": "registry:component",
  "title": "Upload A File To Storage",
  "description": "Upload a file to a storage provider using a presigned upload URL.",
  "dependencies": [
    "@tanstack/react-query"
  ],
  "files": [
    {
      "path": "registry/default/storage/upload-file.tsx",
      "content": "'use client';\n\nimport { type ChangeEvent, useState } from 'react';\nimport { useUploadFile } from '@/registry/default/storage/use-upload-file';\n\nexport function UploadFile() {\n  const [progress, setProgress] = useState(0);\n  const [isUploading, setIsUploading] = useState(false);\n  const { uploadFile } = useUploadFile();\n\n  const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) {\n      return;\n    }\n\n    setIsUploading(true);\n    uploadFile({\n      file,\n      onProgress: (p: number) => setProgress(p),\n      onSuccess: (url: string) => {\n        console.log('Uploaded to:', url);\n        setIsUploading(false);\n        setProgress(0);\n      },\n      onError: (err: Error) => {\n        console.error(`Upload failed: ${err.message}`);\n        setIsUploading(false);\n        setProgress(0);\n      },\n    });\n  };\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <input\n        className=\"file:mr-4 file:rounded-md file:border-0 file:bg-secondary file:px-4 file:py-2 file:font-semibold file:text-secondary-foreground file:text-sm hover:file:bg-secondary/80\"\n        onChange={handleFileChange}\n        type=\"file\"\n      />\n\n      {isUploading && (\n        <div className=\"mt-4 flex items-center gap-2\">\n          <div className=\"h-2 w-full rounded-full bg-secondary\">\n            <div\n              className=\"h-2 rounded-full bg-primary transition-all duration-300\"\n              style={{ width: `${progress}%` }}\n            />\n          </div>\n          <span className=\"text-muted-foreground text-sm\">{progress}%</span>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/storage/use-upload-file.ts",
      "content": "import { useCallback } from 'react';\n\ntype UploadArgs = {\n  file: File;\n  onProgress?: (percent: number) => void;\n  onSuccess?: (fileUrl: string) => void;\n  onError?: (error: Error) => void;\n};\n\nexport function useUploadFile() {\n  const uploadFile = useCallback(\n    async ({ file, onProgress, onSuccess, onError }: UploadArgs) => {\n      try {\n        const presignRes = await fetch('http://localhost:8080/uploadFile', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({\n            fileName: file.name,\n            contentLength: file.size,\n          }),\n        });\n\n        if (!presignRes.ok) {\n          throw new Error('Failed to get presigned URL');\n        }\n        const presign = await presignRes.json();\n\n        await new Promise<void>((resolve, reject) => {\n          const xhr = new XMLHttpRequest();\n          xhr.open('PUT', presign.uploadUrl);\n\n          xhr.setRequestHeader('Content-Type', file.type);\n\n          xhr.upload.onprogress = (evt) => {\n            if (evt.lengthComputable && onProgress) {\n              const PERCENTAGE_MULTIPLIER = 100;\n              const percent = Math.round(\n                (evt.loaded * PERCENTAGE_MULTIPLIER) / evt.total,\n              );\n              onProgress(percent);\n            }\n          };\n\n          xhr.onload = () => {\n            if (xhr.status >= 200 && xhr.status < 300) {\n              resolve();\n            } else {\n              reject(new Error('Upload failed'));\n            }\n          };\n\n          xhr.onerror = () => reject(new Error('Upload failed'));\n          xhr.send(file);\n        });\n\n        const fileUrl = presign.fileUrl as string;\n        onSuccess?.(fileUrl);\n        return fileUrl;\n      } catch (error) {\n        const uploadError =\n          error instanceof Error ? error : new Error('Upload failed');\n        onError?.(uploadError);\n        throw uploadError;\n      }\n    },\n    [],\n  );\n\n  return { uploadFile };\n}\n",
      "type": "registry:hook"
    }
  ],
  "docs": "This component pairs nicely with the `generate-presigned-upload-url` item from this registry to generate a presigned upload URL for a file."
}