{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-file-upload",
  "title": "Use file upload",
  "description": "The use-file-upload hook used by ndekoui components.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/ndeko/hooks/use-file-upload.ts",
      "content": "'use client';\n\nimport type React from 'react';\nimport {\n  useCallback,\n  useRef,\n  useState,\n  type ChangeEvent,\n  type DragEvent,\n  type InputHTMLAttributes,\n} from 'react';\n\nexport type FileMetadata = {\n  name: string;\n  size: number;\n  type: string;\n  url: string;\n  id: string;\n};\n\nexport type FileWithPreview = {\n  file: File | FileMetadata;\n  id: string;\n  preview?: string;\n};\n\nexport type FileUploadOptions = {\n  /** Only read when `multiple` is true. Defaults to Infinity. */\n  maxFiles?: number;\n  /** In bytes. */\n  maxSize?: number;\n  accept?: string;\n  multiple?: boolean;\n  initialFiles?: FileMetadata[];\n  onFilesChange?: (files: FileWithPreview[]) => void;\n  onFilesAdded?: (addedFiles: FileWithPreview[]) => void;\n};\n\nexport type FileUploadState = {\n  files: FileWithPreview[];\n  isDragging: boolean;\n  errors: string[];\n};\n\nexport type FileUploadActions = {\n  addFiles: (files: FileList | File[]) => void;\n  removeFile: (id: string) => void;\n  clearFiles: () => void;\n  clearErrors: () => void;\n  handleDragEnter: (e: DragEvent<HTMLElement>) => void;\n  handleDragLeave: (e: DragEvent<HTMLElement>) => void;\n  handleDragOver: (e: DragEvent<HTMLElement>) => void;\n  handleDrop: (e: DragEvent<HTMLElement>) => void;\n  handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void;\n  openFileDialog: () => void;\n  getInputProps: (\n    props?: InputHTMLAttributes<HTMLInputElement>\n  ) => InputHTMLAttributes<HTMLInputElement> & {\n    ref: React.Ref<HTMLInputElement>;\n  };\n};\n\nfunction sizeErrorOf(isMultiple: boolean, maxSize: number) {\n  const readableSize = formatBytes(maxSize);\n  if (isMultiple) {\n    return `Some files exceed the maximum size of ${readableSize}.`;\n  }\n  return `File exceeds the maximum size of ${readableSize}.`;\n}\n\nexport const useFileUpload = (\n  options: FileUploadOptions = {}\n): [FileUploadState, FileUploadActions] => {\n  const {\n    maxFiles = Infinity,\n    maxSize = Infinity,\n    accept = '*',\n    multiple = false,\n    initialFiles = [],\n    onFilesChange,\n    onFilesAdded,\n  } = options;\n\n  const [state, setState] = useState<FileUploadState>({\n    files: initialFiles.map((file) => ({\n      file,\n      id: file.id,\n      preview: file.url,\n    })),\n    isDragging: false,\n    errors: [],\n  });\n\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const validateFile = useCallback(\n    (file: File | FileMetadata): string | null => {\n      if (file.size > maxSize) {\n        return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`;\n      }\n\n      if (accept !== '*') {\n        const acceptedTypes = accept.split(',').map((type) => type.trim());\n        const fileType = file.type;\n        const extension = file.name.split('.').pop();\n        const fileExtension = `.${extension}`;\n\n        const isAccepted = acceptedTypes.some((type) => {\n          if (type.startsWith('.')) {\n            return fileExtension.toLowerCase() === type.toLowerCase();\n          }\n          if (type.endsWith('/*')) {\n            const baseType = type.split('/')[0];\n            return fileType.startsWith(`${baseType}/`);\n          }\n          return fileType === type;\n        });\n\n        if (!isAccepted) {\n          return `File \"${file.name}\" is not an accepted file type.`;\n        }\n      }\n\n      return null;\n    },\n    [accept, maxSize]\n  );\n\n  const createPreview = useCallback(\n    (file: File | FileMetadata): string | undefined => {\n      if (file instanceof File) {\n        return URL.createObjectURL(file);\n      }\n      return file.url;\n    },\n    []\n  );\n\n  const generateUniqueId = useCallback((file: File | FileMetadata): string => {\n    if (file instanceof File) {\n      return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;\n    }\n    return file.id;\n  }, []);\n\n  const clearFiles = useCallback(() => {\n    setState((prev) => {\n      prev.files.forEach((file) => {\n        if (\n          file.preview &&\n          file.file instanceof File &&\n          file.file.type.startsWith('image/')\n        ) {\n          URL.revokeObjectURL(file.preview);\n        }\n      });\n\n      if (inputRef.current) {\n        inputRef.current.value = '';\n      }\n\n      const newState = {\n        ...prev,\n        files: [],\n        errors: [],\n      };\n\n      onFilesChange?.(newState.files);\n      return newState;\n    });\n  }, [onFilesChange]);\n\n  const addFiles = useCallback(\n    (newFiles: FileList | File[]) => {\n      if (!newFiles || newFiles.length === 0) return;\n\n      const newFilesArray = Array.from(newFiles);\n      const errors: string[] = [];\n\n      setState((prev) => ({ ...prev, errors: [] }));\n\n      if (!multiple) {\n        clearFiles();\n      }\n\n      if (\n        multiple &&\n        maxFiles !== Infinity &&\n        state.files.length + newFilesArray.length > maxFiles\n      ) {\n        errors.push(`You can only upload a maximum of ${maxFiles} files.`);\n        setState((prev) => ({ ...prev, errors }));\n        return;\n      }\n\n      const validFiles: FileWithPreview[] = [];\n\n      newFilesArray.forEach((file) => {\n        if (multiple) {\n          const isDuplicate = state.files.some(\n            (existingFile) =>\n              existingFile.file.name === file.name &&\n              existingFile.file.size === file.size\n          );\n\n          if (isDuplicate) {\n            return;\n          }\n        }\n\n        if (file.size > maxSize) {\n          errors.push(sizeErrorOf(multiple, maxSize));\n          return;\n        }\n\n        const error = validateFile(file);\n        if (error) {\n          errors.push(error);\n        } else {\n          validFiles.push({\n            file,\n            id: generateUniqueId(file),\n            preview: createPreview(file),\n          });\n        }\n      });\n\n      if (validFiles.length > 0) {\n        onFilesAdded?.(validFiles);\n\n        setState((prev) => {\n          let nextFiles = validFiles;\n          if (multiple) {\n            nextFiles = [...prev.files, ...validFiles];\n          }\n          onFilesChange?.(nextFiles);\n          return {\n            ...prev,\n            files: nextFiles,\n            errors,\n          };\n        });\n      } else if (errors.length > 0) {\n        setState((prev) => ({\n          ...prev,\n          errors,\n        }));\n      }\n\n      if (inputRef.current) {\n        inputRef.current.value = '';\n      }\n    },\n    [\n      state.files,\n      maxFiles,\n      multiple,\n      maxSize,\n      validateFile,\n      createPreview,\n      generateUniqueId,\n      clearFiles,\n      onFilesChange,\n      onFilesAdded,\n    ]\n  );\n\n  const removeFile = useCallback(\n    (id: string) => {\n      setState((prev) => {\n        const fileToRemove = prev.files.find((file) => file.id === id);\n        if (\n          fileToRemove &&\n          fileToRemove.preview &&\n          fileToRemove.file instanceof File &&\n          fileToRemove.file.type.startsWith('image/')\n        ) {\n          URL.revokeObjectURL(fileToRemove.preview);\n        }\n\n        const newFiles = prev.files.filter((file) => file.id !== id);\n        onFilesChange?.(newFiles);\n\n        return {\n          ...prev,\n          files: newFiles,\n          errors: [],\n        };\n      });\n    },\n    [onFilesChange]\n  );\n\n  const clearErrors = useCallback(() => {\n    setState((prev) => ({\n      ...prev,\n      errors: [],\n    }));\n  }, []);\n\n  const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setState((prev) => ({ ...prev, isDragging: true }));\n  }, []);\n\n  const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    if (e.currentTarget.contains(e.relatedTarget as Node)) {\n      return;\n    }\n\n    setState((prev) => ({ ...prev, isDragging: false }));\n  }, []);\n\n  const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n  }, []);\n\n  const handleDrop = useCallback(\n    (e: DragEvent<HTMLElement>) => {\n      e.preventDefault();\n      e.stopPropagation();\n      setState((prev) => ({ ...prev, isDragging: false }));\n\n      if (inputRef.current?.disabled) {\n        return;\n      }\n\n      if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n        if (!multiple) {\n          const file = e.dataTransfer.files[0];\n          addFiles([file]);\n        } else {\n          addFiles(e.dataTransfer.files);\n        }\n      }\n    },\n    [addFiles, multiple]\n  );\n\n  const handleFileChange = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      if (e.target.files && e.target.files.length > 0) {\n        addFiles(e.target.files);\n      }\n    },\n    [addFiles]\n  );\n\n  const openFileDialog = useCallback(() => {\n    if (inputRef.current) {\n      inputRef.current.click();\n    }\n  }, []);\n\n  const getInputProps = useCallback(\n    (props: InputHTMLAttributes<HTMLInputElement> = {}) => {\n      return {\n        ...props,\n        type: 'file' as const,\n        onChange: handleFileChange,\n        accept: props.accept || accept,\n        multiple: props.multiple ?? multiple,\n        ref: inputRef,\n      };\n    },\n    [accept, multiple, handleFileChange]\n  );\n\n  return [\n    state,\n    {\n      addFiles,\n      removeFile,\n      clearFiles,\n      clearErrors,\n      handleDragEnter,\n      handleDragLeave,\n      handleDragOver,\n      handleDrop,\n      handleFileChange,\n      openFileDialog,\n      getInputProps,\n    },\n  ];\n};\n\nexport const formatBytes = (bytes: number, decimals = 2): string => {\n  if (bytes === 0) return '0 Bytes';\n\n  const k = 1024;\n  const dm = Math.max(0, decimals);\n  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n  return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i];\n};\n",
      "type": "registry:hook"
    }
  ],
  "categories": [
    "hooks"
  ],
  "type": "registry:hook"
}