{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file",
  "title": "File",
  "author": "vorhdam <https://github.com/vorhdam>",
  "description": "A context-based file uploader with customizable titles, descriptions and icons and a built-in workflow.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "alert",
    "button",
    "progress"
  ],
  "files": [
    {
      "path": "src/components/ui/file.tsx",
      "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\n\r\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Progress } from \"@/components/ui/progress\";\r\nimport { useFiles as useFileContext } from \"@/hooks/use-file\";\r\nimport { messages, Mime, workflow } from \"@/lib/file\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { X } from \"lucide-react\";\r\n\r\nconst contextMap = new Map<string, React.Context<FileContext>>();\r\n\r\nexport function bindContext(providerId: string): React.Context<FileContext> {\r\n  if (!contextMap.has(providerId))\r\n    contextMap.set(\r\n      providerId,\r\n      React.createContext<FileContext>(DefaultFileContext),\r\n    );\r\n  return contextMap.get(providerId)!;\r\n}\r\n\r\ntype Metadata = Pick<File, \"name\" | \"size\" | \"type\">;\r\n\r\ntype FileContext = {\r\n  providerId: string;\r\n  maxFiles: number;\r\n  maxSize: number;\r\n  accept: Mime[];\r\n  files: File[];\r\n  metadatas: Metadata[];\r\n  error: string | null;\r\n  progress: number | null;\r\n  addFiles: (files: FileList | null) => void;\r\n  removeFile: (index: number) => void;\r\n  clearFiles: () => void;\r\n};\r\n\r\ntype FileProviderProps = {\r\n  providerId: string;\r\n  maxFiles: number;\r\n  maxSize: number;\r\n  accept: Mime[];\r\n  children: React.ReactNode;\r\n};\r\n\r\nconst DefaultFileContext: FileContext = {\r\n  providerId: \"\",\r\n  maxFiles: 1, // 1 File\r\n  maxSize: 1024 * 1024, // 1 MB\r\n  accept: [],\r\n  files: [],\r\n  metadatas: [],\r\n  error: null,\r\n  progress: null,\r\n  addFiles: () => {},\r\n  removeFile: () => {},\r\n  clearFiles: () => {},\r\n};\r\n\r\nfunction getFileSize(bytes: number) {\r\n  if (!bytes || bytes === 0) return \"0 B\";\r\n  const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"];\r\n  const i = Math.floor(Math.log(bytes) / Math.log(1024));\r\n  const size = bytes / Math.pow(1024, i);\r\n  return `${size.toFixed(size % 1 === 0 ? 0 : 2)} ${units[i]}`;\r\n}\r\n\r\nfunction FileProvider({\r\n  providerId,\r\n  maxFiles,\r\n  maxSize,\r\n  accept,\r\n  children,\r\n}: FileProviderProps) {\r\n  const [files, setFiles] = React.useState<File[]>([]);\r\n  const [metadatas, setMetadatas] = React.useState<Metadata[]>([]);\r\n  const [error, setError] = React.useState<string | null>(null);\r\n  const [progress, setProgress] = React.useState<number | null>(null);\r\n\r\n  const addFiles = (fileList: FileList | null) => {\r\n    setError(null);\r\n\r\n    if (!fileList || fileList.length === 0)\r\n      return setError(messages[\"noFiles\"]);\r\n    const newFiles: File[] = Array.from(fileList);\r\n\r\n    if (files.length + newFiles.length > maxFiles)\r\n      return setError(messages[\"tooManyFiles\"]);\r\n\r\n    setProgress(0);\r\n\r\n    try {\r\n      for (const file of newFiles) {\r\n        const result = workflow({ file, maxSize, accept });\r\n        if (!result.success) return setError(result.message);\r\n\r\n        const metadata: Metadata = {\r\n          name: file.name,\r\n          size: file.size,\r\n          type: file.type,\r\n        };\r\n\r\n        setFiles((prev) => [...prev, file]);\r\n        setMetadatas((prev) => [...prev, metadata]);\r\n\r\n        setProgress(\r\n          (prev) => Number(prev) + Number((100 / newFiles.length).toFixed(0)),\r\n        );\r\n      }\r\n    } catch (error) {\r\n      setError(messages[\"unexpected\"]);\r\n      console.error(\"An error occured while validating files\", error);\r\n    } finally {\r\n      setProgress(null);\r\n    }\r\n  };\r\n\r\n  const removeFile = (index: number) => {\r\n    if (index >= 0 && index < files.length) {\r\n      setFiles((prev) => prev.filter((_, i) => i !== index));\r\n      setMetadatas((prev) => prev.filter((_, i) => i !== index));\r\n      setError(null);\r\n    }\r\n  };\r\n\r\n  const clearFiles = () => {\r\n    setFiles([]);\r\n    setMetadatas([]);\r\n    setError(null);\r\n  };\r\n\r\n  const values: FileContext = {\r\n    providerId,\r\n    maxFiles,\r\n    maxSize,\r\n    accept,\r\n    files,\r\n    metadatas,\r\n    error,\r\n    progress,\r\n    addFiles,\r\n    removeFile,\r\n    clearFiles,\r\n  };\r\n\r\n  const Context = bindContext(providerId);\r\n\r\n  return <Context.Provider value={values}>{children}</Context.Provider>;\r\n}\r\n\r\nconst preventDefault = (e: React.DragEvent) => e.preventDefault();\r\nconst FileInputContext = React.createContext<string | null>(null);\r\n\r\nconst useFiles = () => {\r\n  const id = React.useContext(FileInputContext);\r\n  if (!id) throw new Error(\"useFiles should be in a file provider\");\r\n  return useFileContext(id);\r\n};\r\n\r\nfunction FileInput({\r\n  providerId,\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"form\"> & {\r\n  providerId: string;\r\n}) {\r\n  return (\r\n    <FileInputContext.Provider value={providerId}>\r\n      <form data-slot=\"file-input\" className={className} {...props}>\r\n        {children}\r\n      </form>\r\n    </FileInputContext.Provider>\r\n  );\r\n}\r\n\r\nfunction FileField({\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"input\">) {\r\n  const { providerId, metadatas, maxFiles, accept, addFiles } = useFiles();\r\n\r\n  const handleFileAdd = (event: React.ChangeEvent<HTMLInputElement>) => {\r\n    addFiles(event.target.files);\r\n    event.target.value = \"\";\r\n  };\r\n\r\n  const handleFileDrop = (event: React.DragEvent<HTMLInputElement>) => {\r\n    preventDefault(event);\r\n    addFiles(event.dataTransfer.files);\r\n  };\r\n\r\n  const handleClick = () => {\r\n    document.getElementById(`file-input-${providerId}`)?.click();\r\n  };\r\n\r\n  return (\r\n    <div hidden={metadatas.length > 0}>\r\n      <input\r\n        id={`file-input-${providerId}`}\r\n        type=\"file\"\r\n        name={metadatas.length === 0 ? \"files\" : undefined}\r\n        hidden\r\n        accept={accept.join(\", \")}\r\n        multiple={maxFiles > 1}\r\n        onChange={handleFileAdd}\r\n        onDrop={handleFileDrop}\r\n        {...props}\r\n      />\r\n      {metadatas.length > 0 && (\r\n        <input\r\n          type=\"hidden\"\r\n          name=\"files\"\r\n          value={JSON.stringify(metadatas)}\r\n          readOnly\r\n        />\r\n      )}\r\n      <div\r\n        data-slot=\"file-input-field\"\r\n        className={cn(\r\n          \"flex flex-col justify-center items-center gap-4 bg-card text-card-foreground border border-dashed border-border min-h-48 max-h-60 rounded-2xl py-6 shadow-sm cursor-pointer\",\r\n          className,\r\n        )}\r\n        onClick={handleClick}\r\n        onDrop={handleFileDrop}\r\n        onDragOver={preventDefault}\r\n        onDragEnter={preventDefault}\r\n        onDragLeave={preventDefault}\r\n      >\r\n        {children}\r\n      </div>\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction FileList({ className, ...props }: React.ComponentProps<\"div\">) {\r\n  const { files, removeFile } = useFiles();\r\n\r\n  return (\r\n    <div\r\n      hidden={files.length === 0}\r\n      data-slot=\"file-input-list\"\r\n      className={cn(\r\n        \"flex flex-col h-fit max-h-60 p-2 rounded-3xl border-border border bg-card\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    >\r\n      <div className=\"flex flex-col gap-2 overflow-auto w-full h-full rounded-2xl\">\r\n        {files.map((file, index) => (\r\n          <FileItem\r\n            key={index}\r\n            file={file}\r\n            onRemove={() => removeFile(index)}\r\n          />\r\n        ))}\r\n      </div>\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction FileItem({ file, onRemove }: { file: File; onRemove: () => void }) {\r\n  return (\r\n    <div\r\n      data-slot=\"file-input-item\"\r\n      className=\"flex flex-row justify-between items-center gap-2 p-2 pl-4 bg-muted/75 text-card-foreground w-full rounded-2xl\"\r\n    >\r\n      <div className=\"flex flex-col\">\r\n        <span className=\"truncate\">{file.name}</span>\r\n        <span className=\"text-xs text-muted-foreground\">\r\n          {getFileSize(file.size)}\r\n        </span>\r\n      </div>\r\n      <Button\r\n        type=\"button\"\r\n        variant=\"secondary\"\r\n        size=\"icon\"\r\n        className=\"rounded-full cursor-pointer bg-muted-foreground/15\"\r\n        onClick={onRemove}\r\n      >\r\n        <X />\r\n      </Button>\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction FileError({\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"div\">) {\r\n  const { error } = useFiles();\r\n\r\n  return (\r\n    <Alert hidden={!error} className=\"mt-2\">\r\n      <AlertDescription\r\n        data-slot=\"file-input-error\"\r\n        className={cn(\"leading-none text-sm text-destructive\", className)}\r\n        {...props}\r\n      >\r\n        {error ? error : children}\r\n      </AlertDescription>\r\n    </Alert>\r\n  );\r\n}\r\n\r\nfunction FileSubmit({ className, ...props }: React.ComponentProps<\"button\">) {\r\n  const { progress, metadatas } = useFiles();\r\n\r\n  return (\r\n    <Button\r\n      type=\"submit\"\r\n      variant=\"default\"\r\n      className={cn(\"mt-4 w-full\", className)}\r\n      disabled={Boolean(progress) || metadatas.length === 0}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction FileContent({ className, ...props }: React.ComponentProps<\"div\">) {\r\n  const { progress } = useFiles();\r\n\r\n  return (\r\n    <div\r\n      hidden={Boolean(progress)}\r\n      data-slot=\"file-input-content\"\r\n      className={cn(\r\n        \"flex flex-col justify-center items-center text-center gap-2 px-6\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction FileIcon({\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"div\">) {\r\n  return (\r\n    <div\r\n      data-slot=\"file-input-icon\"\r\n      className={cn(\"size-8 text-primary\", className)}\r\n      {...props}\r\n    >\r\n      {children}\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction FileTitle({ className, ...props }: React.ComponentProps<\"div\">) {\r\n  return (\r\n    <div\r\n      data-slot=\"file-input-title\"\r\n      className={cn(\"leading-none text-lg font-semibold\", className)}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction FileDescription({ className, ...props }: React.ComponentProps<\"div\">) {\r\n  return (\r\n    <div\r\n      data-slot=\"file-input-description\"\r\n      className={cn(\"leading-none text-sm text-muted-foreground\", className)}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction FileLoader({\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"div\">) {\r\n  const { progress } = useFiles();\r\n\r\n  if (!progress) return null;\r\n\r\n  return (\r\n    <div\r\n      data-slot=\"file-input-loader\"\r\n      {...props}\r\n      className=\"flex flex-col gap-4 min-w-56 text-center items-center justify-center\"\r\n    >\r\n      <div className={cn(\"leading-none text-lg font-semibold\", className)}>\r\n        {children}\r\n      </div>\r\n      <div>{progress}%</div>\r\n      <Progress value={progress} />\r\n    </div>\r\n  );\r\n}\r\n\r\nexport {\r\n  FileContent,\r\n  FileDescription,\r\n  FileError,\r\n  FileField,\r\n  FileIcon,\r\n  FileInput,\r\n  FileList,\r\n  FileLoader,\r\n  FileProvider,\r\n  FileSubmit,\r\n  FileTitle,\r\n};\r\n",
      "type": "registry:ui"
    },
    {
      "path": "src/hooks/use-file.ts",
      "content": "import { bindContext } from \"@/components/ui/file\";\r\nimport { useContext } from \"react\";\r\n\r\nexport function useFiles(providerId: string) {\r\n  const Context = bindContext(providerId);\r\n  const context = useContext(Context);\r\n  if (!context)\r\n    throw new Error(\r\n      `The useFiles() hook must be used within the boundaries of a FileProvider with id: \"${providerId}\".`,\r\n    );\r\n  return context;\r\n}\r\n",
      "type": "registry:hook"
    },
    {
      "path": "src/lib/file.ts",
      "content": "const Mimes = {\r\n  application: [\r\n    \"pdf\",\r\n    \"msword\",\r\n    \"vnd.openxmlformats-officedocument.wordprocessingml.document\",\r\n    \"vnd.ms-excel\",\r\n    \"vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\r\n    \"vnd.ms-powerpoint\",\r\n    \"vnd.openxmlformats-officedocument.presentationml.presentation\",\r\n  ],\r\n  video: [\"webm\", \"mp4\", \"mpeg\", \"ogg\", \"quicktime\"],\r\n  image: [\"webp\", \"jpeg\", \"png\", \"gif\", \"heic\", \"heif\"],\r\n  text: [\"plain\", \"csv\", \"xml\"],\r\n} as const satisfies Record<string, string[]>;\r\n\r\ntype MimeCategory = keyof typeof Mimes;\r\n\r\ntype Mime =\r\n  | {\r\n      [category in MimeCategory]: `${category}/${(typeof Mimes)[category][number]}`;\r\n    }[MimeCategory]\r\n  | (string & {});\r\n\r\nconst messages = {\r\n  sizeLarge: \"Your file is too large.\",\r\n  typeInvalid: \"Your file has an invalid type.\",\r\n  tooManyFiles: \"You are uploading too many files.\",\r\n  noFiles: \"You didn't upload any files.\",\r\n  unexpected: \"An unexpected error occured.\",\r\n};\r\n\r\ntype WorkflowProps = {\r\n  file: File;\r\n  maxSize: number;\r\n  accept: Mime[];\r\n};\r\n\r\ntype WorkflowResult =\r\n  | {\r\n      success: true;\r\n      file: File;\r\n    }\r\n  | {\r\n      success: false;\r\n      message: string;\r\n    };\r\n\r\n/**\r\n * This workflow executes everytime a user adds a new file to your input (feel free to add other params you need)\r\n * @param file The file that the user uploaded\r\n * @param maxSize The max size you allowed in the file context provider\r\n * @param accept The mimes the user can upload you allowed in the file context provider\r\n */\r\nfunction workflow({ file, maxSize, accept }: WorkflowProps): WorkflowResult {\r\n  // Add your validation, compression or any other file logic here:\r\n  if (file.size > maxSize)\r\n    return { success: false, message: messages[\"sizeLarge\"] };\r\n  if (!accept.includes(file.type))\r\n    return { success: false, message: messages[\"typeInvalid\"] };\r\n\r\n  return {\r\n    success: true,\r\n    file: file,\r\n  };\r\n}\r\n\r\nexport { messages, Mimes, workflow, type Mime };\r\n",
      "type": "registry:lib"
    }
  ],
  "css": {
    "::-webkit-scrollbar": {
      "width": "1.2rem",
      "min-height": "1.2rem"
    },
    "::-webkit-scrollbar-thumb": {
      "border": "0.4rem solid rgba(0, 0, 0, 0)",
      "background-clip": "padding-box",
      "background-color": "var(--border)",
      "border-radius": "var(--radius)"
    },
    "::-webkit-scrollbar-thumb:hover": {
      "background-color": "var(--ring)"
    }
  },
  "type": "registry:block"
}