{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "uploader-provider",
  "type": "registry:component",
  "title": "Uploader Provider",
  "description": "A provider component and hook for managing file uploads with progress tracking and state management.",
  "files": [
    {
      "path": "examples/components/src/components/upload/uploader-provider.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\n/**\n * Represents the possible statuses of a file in the uploader.\n */\nexport type FileStatus = 'PENDING' | 'UPLOADING' | 'COMPLETE' | 'ERROR';\n\n/**\n * Represents the state of a file in the uploader.\n */\nexport type FileState = {\n  /** The file object being uploaded */\n  file: File;\n\n  /** Unique identifier for the file */\n  key: string;\n\n  /** Upload progress (0-100) */\n  progress: number;\n\n  /** Current status of the file */\n  status: FileStatus;\n\n  /** URL of the uploaded file (available when status is COMPLETE) */\n  url?: string;\n\n  /** Error message if the upload failed */\n  error?: string;\n\n  /** AbortController to cancel the upload */\n  abortController?: AbortController;\n\n  /** Whether the file should be automatically uploaded */\n  autoUpload?: boolean;\n};\n\n/**\n * Represents a file that has completed uploading.\n */\nexport type CompletedFileState = Omit<FileState, 'status' | 'url'> & {\n  /** Status is guaranteed to be 'COMPLETE' */\n  status: 'COMPLETE';\n\n  /** URL is guaranteed to be available */\n  url: string;\n};\n\n/**\n * Function type for handling file uploads.\n */\nexport type UploadFn<TOptions = unknown> = (props: {\n  /** The file to be uploaded */\n  file: File;\n\n  /** AbortSignal to cancel the upload */\n  signal: AbortSignal;\n\n  /** Callback to update progress */\n  onProgressChange: (progress: number) => void | Promise<void>;\n\n  /** Additional options */\n  options?: TOptions;\n}) => Promise<{ url: string }>;\n\n/**\n * Context type for the UploaderProvider.\n */\ntype UploaderContextType<TOptions = unknown> = {\n  /** List of all files in the uploader */\n  fileStates: FileState[];\n\n  /** Add files to the uploader */\n  addFiles: (files: File[]) => void;\n\n  /** Update a file's state */\n  updateFileState: (key: string, changes: Partial<FileState>) => void;\n\n  /** Remove a file from the uploader */\n  removeFile: (key: string) => void;\n\n  /** Cancel an ongoing upload */\n  cancelUpload: (key: string) => void;\n\n  /** Start uploading files */\n  uploadFiles: (keysToUpload?: string[], options?: TOptions) => Promise<void>;\n\n  /** Reset all files */\n  resetFiles: () => void;\n\n  /** Whether any file is currently uploading */\n  isUploading: boolean;\n\n  /** Whether files should be automatically uploaded */\n  autoUpload?: boolean;\n};\n\n/**\n * Props for the UploaderProvider component.\n */\ntype ProviderProps<TOptions = unknown> = {\n  /** React children or render function */\n  children:\n    | React.ReactNode\n    | ((context: UploaderContextType<TOptions>) => React.ReactNode);\n\n  /** Callback when files change */\n  onChange?: (args: {\n    allFiles: FileState[];\n    completedFiles: CompletedFileState[];\n  }) => void | Promise<void>;\n\n  /** Callback when a file is added */\n  onFileAdded?: (file: FileState) => void | Promise<void>;\n\n  /** Callback when a file is removed */\n  onFileRemoved?: (key: string) => void | Promise<void>;\n\n  /** Callback when a file upload completes */\n  onUploadCompleted?: (file: CompletedFileState) => void | Promise<void>;\n\n  /** Function to handle the actual upload */\n  uploadFn: UploadFn<TOptions>;\n\n  /** External value to control the file states */\n  value?: FileState[];\n\n  /** Whether files should be automatically uploaded when added */\n  autoUpload?: boolean;\n};\n\n// Context\nconst UploaderContext =\n  React.createContext<UploaderContextType<unknown> | null>(null);\n\n/**\n * Hook to access the uploader context.\n *\n * @returns The uploader context\n * @throws Error if used outside of UploaderProvider\n *\n * @example\n * ```tsx\n * const { fileStates, addFiles, uploadFiles } = useUploader();\n * ```\n */\nexport function useUploader<TOptions = unknown>() {\n  const context = React.useContext(UploaderContext);\n  if (!context) {\n    throw new Error('useUploader must be used within a UploaderProvider');\n  }\n  return context as UploaderContextType<TOptions>;\n}\n\n/**\n * Provider component for file upload functionality.\n *\n * @component\n * @example\n * ```tsx\n * <UploaderProvider\n *   uploadFn={async ({ file, signal, onProgressChange }) => {\n *     // Upload implementation\n *     return { url: 'https://example.com/uploads/image.jpg' };\n *   }}\n *   autoUpload={true}\n * >\n *   <ImageUploader maxFiles={5} maxSize={1024 * 1024 * 2} />\n * </UploaderProvider>\n * ```\n */\nexport function UploaderProvider<TOptions = unknown>({\n  children,\n  onChange,\n  onFileAdded,\n  onFileRemoved,\n  onUploadCompleted,\n  uploadFn,\n  value: externalValue,\n  autoUpload = false,\n}: ProviderProps<TOptions>) {\n  const [fileStates, setFileStates] = React.useState<FileState[]>(\n    externalValue ?? [],\n  );\n  const [pendingAutoUploadKeys, setPendingAutoUploadKeys] = React.useState<\n    string[] | null\n  >(null);\n\n  // Sync with external value if provided\n  React.useEffect(() => {\n    if (externalValue) {\n      setFileStates(externalValue);\n    }\n  }, [externalValue]);\n\n  const updateFileState = React.useCallback(\n    (key: string, changes: Partial<FileState>) => {\n      setFileStates((prevStates) => {\n        return prevStates.map((fileState) => {\n          if (fileState.key === key) {\n            return { ...fileState, ...changes };\n          }\n          return fileState;\n        });\n      });\n    },\n    [],\n  );\n\n  const uploadFiles = React.useCallback(\n    async (keysToUpload?: string[], options?: TOptions) => {\n      const filesToUpload = fileStates.filter(\n        (fileState) =>\n          fileState.status === 'PENDING' &&\n          (!keysToUpload || keysToUpload.includes(fileState.key)),\n      );\n\n      if (filesToUpload.length === 0) return;\n\n      await Promise.all(\n        filesToUpload.map(async (fileState) => {\n          try {\n            const abortController = new AbortController();\n            updateFileState(fileState.key, {\n              abortController,\n              status: 'UPLOADING',\n              progress: 0,\n            });\n\n            const uploadResult = await uploadFn({\n              file: fileState.file,\n              signal: abortController.signal,\n              onProgressChange: (progress) => {\n                updateFileState(fileState.key, { progress });\n              },\n              options,\n            });\n\n            // Wait a bit to show the bar at 100%\n            await new Promise((resolve) => setTimeout(resolve, 500));\n\n            const completedFile = {\n              ...fileState,\n              status: 'COMPLETE' as const,\n              progress: 100,\n              url: uploadResult?.url,\n            };\n\n            updateFileState(fileState.key, {\n              status: 'COMPLETE',\n              progress: 100,\n              url: uploadResult?.url,\n            });\n\n            // Call onUploadCompleted when a file upload is completed\n            if (onUploadCompleted) {\n              void onUploadCompleted(completedFile);\n            }\n          } catch (err: unknown) {\n            if (\n              err instanceof Error &&\n              // if using with EdgeStore, the error name is UploadAbortedError\n              (err.name === 'AbortError' || err.name === 'UploadAbortedError')\n            ) {\n              updateFileState(fileState.key, {\n                status: 'PENDING',\n                progress: 0,\n                error: 'Upload canceled',\n              });\n            } else {\n              if (process.env.NODE_ENV === 'development') {\n                console.error(err);\n              }\n              const errorMessage =\n                err instanceof Error ? err.message : 'Upload failed';\n              updateFileState(fileState.key, {\n                status: 'ERROR',\n                error: errorMessage,\n              });\n            }\n          }\n        }),\n      );\n    },\n    [fileStates, updateFileState, uploadFn, onUploadCompleted],\n  );\n\n  const addFiles = React.useCallback(\n    (files: File[]) => {\n      const newFileStates = files.map<FileState>((file) => ({\n        file,\n        key: `${file.name}-${Date.now()}-${Math.random()\n          .toString(36)\n          .slice(2)}`,\n        progress: 0,\n        status: 'PENDING',\n        autoUpload,\n      }));\n      setFileStates((prev) => [...prev, ...newFileStates]);\n\n      // Call onFileAdded for each new file\n      if (onFileAdded) {\n        newFileStates.forEach((fileState) => {\n          void onFileAdded(fileState);\n        });\n      }\n\n      if (autoUpload) {\n        setPendingAutoUploadKeys(newFileStates.map((fs) => fs.key));\n      }\n    },\n    [autoUpload, onFileAdded],\n  );\n\n  const removeFile = React.useCallback(\n    (key: string) => {\n      setFileStates((prev) =>\n        prev.filter((fileState) => fileState.key !== key),\n      );\n\n      // Call onFileRemoved when a file is removed\n      if (onFileRemoved) {\n        void onFileRemoved(key);\n      }\n    },\n    [onFileRemoved],\n  );\n\n  const cancelUpload = React.useCallback(\n    (key: string) => {\n      const fileState = fileStates.find((f) => f.key === key);\n      if (fileState?.abortController && fileState.progress < 100) {\n        fileState.abortController.abort();\n        if (fileState?.autoUpload) {\n          // Remove file if it was an auto-upload\n          removeFile(key);\n        } else {\n          // If it was not an auto-upload, reset the file state\n          updateFileState(key, { status: 'PENDING', progress: 0 });\n        }\n      }\n    },\n    [fileStates, updateFileState, removeFile],\n  );\n\n  const resetFiles = React.useCallback(() => {\n    setFileStates([]);\n  }, []);\n\n  React.useEffect(() => {\n    const completedFileStates = fileStates.filter(\n      (fs): fs is CompletedFileState => fs.status === 'COMPLETE' && !!fs.url,\n    );\n    void onChange?.({\n      allFiles: fileStates,\n      completedFiles: completedFileStates,\n    });\n  }, [fileStates, onChange]);\n\n  // Handle auto-uploading files added to the queue\n  React.useEffect(() => {\n    if (pendingAutoUploadKeys && pendingAutoUploadKeys.length > 0) {\n      void uploadFiles(pendingAutoUploadKeys);\n      setPendingAutoUploadKeys(null);\n    }\n  }, [pendingAutoUploadKeys, uploadFiles]);\n\n  const isUploading = React.useMemo(\n    () => fileStates.some((fs) => fs.status === 'UPLOADING'),\n    [fileStates],\n  );\n\n  const value = React.useMemo(\n    () => ({\n      fileStates,\n      addFiles,\n      updateFileState,\n      removeFile,\n      cancelUpload,\n      uploadFiles,\n      resetFiles,\n      isUploading,\n      autoUpload,\n    }),\n    [\n      fileStates,\n      addFiles,\n      updateFileState,\n      removeFile,\n      cancelUpload,\n      uploadFiles,\n      resetFiles,\n      isUploading,\n      autoUpload,\n    ],\n  );\n\n  return (\n    <UploaderContext.Provider value={value as UploaderContextType<unknown>}>\n      {typeof children === 'function' ? children(value) : children}\n    </UploaderContext.Provider>\n  );\n}\n\n/**\n * Formats a file size in bytes to a human-readable string.\n *\n * @param bytes - The file size in bytes\n * @returns A formatted string (e.g., \"1.5 MB\")\n *\n * @example\n * ```ts\n * formatFileSize(1024); // \"1 KB\"\n * formatFileSize(1024 * 1024 * 2.5); // \"2.5 MB\"\n * ```\n */\nexport function formatFileSize(bytes?: number) {\n  if (!bytes) return '0 B';\n  const k = 1024;\n  const dm = 2;\n  const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;\n}\n",
      "type": "registry:component",
      "target": "components/upload/uploader-provider.tsx"
    }
  ]
}