{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-file-upload",
  "title": "file upload hook",
  "description": "state management hook for file upload component",
  "files": [
    {
      "path": "src/form-builder/hooks/use-file-upload.tsx",
      "content": "/** biome-ignore-all lint/correctness/useExhaustiveDependencies: <explanation> */\nimport type React from 'react'\n\nimport {\n\ttype ChangeEvent,\n\ttype DragEvent,\n\ttype InputHTMLAttributes,\n\tuseCallback,\n\tuseRef,\n\tuseState,\n} from 'react'\n\nexport type FileMetadata = {\n\tname: string\n\tsize: number\n\ttype: string\n\turl: string\n\tid: string\n}\n\nexport type FileWithPreview = {\n\tfile: File | FileMetadata\n\tid: string\n\tpreview?: string\n}\n\nexport type FileUploadOptions = {\n\tmaxFiles?: number // Only used when multiple is true, defaults to Infinity\n\tmaxSize?: number // in bytes\n\taccept?: string\n\tmultiple?: boolean // Defaults to false\n\tinitialFiles?: FileMetadata[]\n\tonFilesChange?: (files: FileWithPreview[]) => void // Callback when files change\n\tonFilesAdded?: (addedFiles: FileWithPreview[]) => void // Callback when new files are added\n}\n\nexport type FileUploadState = {\n\tfiles: FileWithPreview[]\n\tisDragging: boolean\n\terrors: string[]\n}\n\nexport type FileUploadActions = {\n\taddFiles: (files: FileList | File[]) => void\n\tremoveFile: (id: string) => void\n\tclearFiles: () => void\n\tclearErrors: () => void\n\thandleDragEnter: (e: DragEvent<HTMLElement>) => void\n\thandleDragLeave: (e: DragEvent<HTMLElement>) => void\n\thandleDragOver: (e: DragEvent<HTMLElement>) => void\n\thandleDrop: (e: DragEvent<HTMLElement>) => void\n\thandleFileChange: (e: ChangeEvent<HTMLInputElement>) => void\n\topenFileDialog: () => void\n\tgetInputProps: (\n\t\tprops?: InputHTMLAttributes<HTMLInputElement>,\n\t) => InputHTMLAttributes<HTMLInputElement> & {\n\t\tref: React.Ref<HTMLInputElement>\n\t}\n}\n\nexport const useFileUpload = (\n\toptions: FileUploadOptions = {},\n): [FileUploadState, FileUploadActions] => {\n\tconst {\n\t\tmaxFiles = Infinity,\n\t\tmaxSize = Infinity,\n\t\taccept = '*',\n\t\tmultiple = false,\n\t\tinitialFiles = [],\n\t\tonFilesChange,\n\t\tonFilesAdded,\n\t} = options\n\n\tconst [state, setState] = useState<FileUploadState>({\n\t\tfiles: initialFiles.map((file) => ({\n\t\t\tfile,\n\t\t\tid: file.id,\n\t\t\tpreview: file.url,\n\t\t})),\n\t\tisDragging: false,\n\t\terrors: [],\n\t})\n\n\tconst inputRef = useRef<HTMLInputElement>(null)\n\n\tconst createPreview = useCallback(\n\t\t(file: File | FileMetadata): string | undefined => {\n\t\t\tif (file instanceof File) {\n\t\t\t\treturn URL.createObjectURL(file)\n\t\t\t}\n\t\t\treturn file.url\n\t\t},\n\t\t[],\n\t)\n\n\tconst generateUniqueId = useCallback((file: File | FileMetadata): string => {\n\t\tif (file instanceof File) {\n\t\t\treturn `${file.name}-${crypto.randomUUID()}`\n\t\t}\n\t\treturn file.id\n\t}, [])\n\n\tconst clearFiles = useCallback(() => {\n\t\tsetState((prev) => {\n\t\t\t// Clean up object URLs\n\t\t\tprev.files.forEach((file) => {\n\t\t\t\tif (\n\t\t\t\t\tfile.preview &&\n\t\t\t\t\tfile.file instanceof File &&\n\t\t\t\t\tfile.file.type.startsWith('image/')\n\t\t\t\t) {\n\t\t\t\t\tURL.revokeObjectURL(file.preview)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif (inputRef.current) {\n\t\t\t\tinputRef.current.value = ''\n\t\t\t}\n\n\t\t\tconst newState = {\n\t\t\t\t...prev,\n\t\t\t\tfiles: [],\n\t\t\t\terrors: [],\n\t\t\t}\n\n\t\t\tonFilesChange?.(newState.files)\n\t\t\treturn newState\n\t\t})\n\t}, [onFilesChange])\n\n\tconst addFiles = useCallback(\n\t\t(newFiles: FileList | File[]) => {\n\t\t\tif (!newFiles || newFiles.length === 0) return\n\n\t\t\tconst newFilesArray = Array.from(newFiles)\n\t\t\tconst errors: string[] = []\n\n\t\t\t// Clear existing errors when new files are uploaded\n\t\t\tsetState((prev) => ({ ...prev, errors: [] }))\n\n\t\t\t// In single file mode, clear existing files first\n\t\t\tif (!multiple) {\n\t\t\t\tclearFiles()\n\t\t\t}\n\n\t\t\t// Check if adding these files would exceed maxFiles (only in multiple mode)\n\t\t\tif (\n\t\t\t\tmultiple &&\n\t\t\t\tmaxFiles !== Infinity &&\n\t\t\t\tstate.files.length + newFilesArray.length > maxFiles\n\t\t\t) {\n\t\t\t\terrors.push(`You can only upload a maximum of ${maxFiles} files.`)\n\t\t\t\tsetState((prev) => ({ ...prev, errors }))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst validFiles: FileWithPreview[] = []\n\n\t\t\tnewFilesArray.forEach((file) => {\n\t\t\t\t// Only check for duplicates if multiple files are allowed\n\t\t\t\tif (multiple) {\n\t\t\t\t\tconst isDuplicate = state.files.some(\n\t\t\t\t\t\t(existingFile) =>\n\t\t\t\t\t\t\texistingFile.file.name === file.name &&\n\t\t\t\t\t\t\texistingFile.file.size === file.size,\n\t\t\t\t\t)\n\n\t\t\t\t\t// Skip duplicate files silently\n\t\t\t\t\tif (isDuplicate) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tvalidFiles.push({\n\t\t\t\t\tfile,\n\t\t\t\t\tid: generateUniqueId(file),\n\t\t\t\t\tpreview: createPreview(file),\n\t\t\t\t})\n\t\t\t})\n\n\t\t\t// Only update state if we have valid files to add\n\t\t\tif (validFiles.length > 0) {\n\t\t\t\t// Call the onFilesAdded callback with the newly added valid files\n\t\t\t\tonFilesAdded?.(validFiles)\n\n\t\t\t\tsetState((prev) => {\n\t\t\t\t\tconst newFiles = !multiple\n\t\t\t\t\t\t? validFiles\n\t\t\t\t\t\t: [...prev.files, ...validFiles]\n\t\t\t\t\tonFilesChange?.(newFiles)\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...prev,\n\t\t\t\t\t\tfiles: newFiles,\n\t\t\t\t\t\terrors,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t} else if (errors.length > 0) {\n\t\t\t\tsetState((prev) => ({\n\t\t\t\t\t...prev,\n\t\t\t\t\terrors,\n\t\t\t\t}))\n\t\t\t}\n\n\t\t\t// Reset input value after handling files\n\t\t\tif (inputRef.current) {\n\t\t\t\tinputRef.current.value = ''\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tstate.files,\n\t\t\tmaxFiles,\n\t\t\tmultiple,\n\t\t\tmaxSize,\n\t\t\t// validateFile,\n\t\t\tcreatePreview,\n\t\t\tgenerateUniqueId,\n\t\t\tclearFiles,\n\t\t\tonFilesChange,\n\t\t\tonFilesAdded,\n\t\t],\n\t)\n\n\tconst removeFile = useCallback(\n\t\t(id: string) => {\n\t\t\tsetState((prev) => {\n\t\t\t\tconst fileToRemove = prev.files.find((file) => file.id === id)\n\t\t\t\tif (\n\t\t\t\t\tfileToRemove &&\n\t\t\t\t\tfileToRemove.preview &&\n\t\t\t\t\tfileToRemove.file instanceof File &&\n\t\t\t\t\tfileToRemove.file.type.startsWith('image/')\n\t\t\t\t) {\n\t\t\t\t\tURL.revokeObjectURL(fileToRemove.preview)\n\t\t\t\t}\n\n\t\t\t\tconst newFiles = prev.files.filter((file) => file.id !== id)\n\t\t\t\tonFilesChange?.(newFiles)\n\n\t\t\t\treturn {\n\t\t\t\t\t...prev,\n\t\t\t\t\tfiles: newFiles,\n\t\t\t\t\terrors: [],\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[onFilesChange],\n\t)\n\n\tconst clearErrors = useCallback(() => {\n\t\tsetState((prev) => ({\n\t\t\t...prev,\n\t\t\terrors: [],\n\t\t}))\n\t}, [])\n\n\tconst handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {\n\t\te.preventDefault()\n\t\te.stopPropagation()\n\t\tsetState((prev) => ({ ...prev, isDragging: true }))\n\t}, [])\n\n\tconst handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {\n\t\te.preventDefault()\n\t\te.stopPropagation()\n\n\t\tif (e.currentTarget.contains(e.relatedTarget as Node)) {\n\t\t\treturn\n\t\t}\n\n\t\tsetState((prev) => ({ ...prev, isDragging: false }))\n\t}, [])\n\n\tconst handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {\n\t\te.preventDefault()\n\t\te.stopPropagation()\n\t}, [])\n\n\tconst handleDrop = useCallback(\n\t\t(e: DragEvent<HTMLElement>) => {\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t\tsetState((prev) => ({ ...prev, isDragging: false }))\n\n\t\t\t// Don't process files if the input is disabled\n\t\t\tif (inputRef.current?.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n\t\t\t\t// In single file mode, only use the first file\n\t\t\t\tif (!multiple) {\n\t\t\t\t\tconst file = e.dataTransfer.files[0]\n\t\t\t\t\taddFiles([file])\n\t\t\t\t} else {\n\t\t\t\t\taddFiles(e.dataTransfer.files)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[addFiles, multiple],\n\t)\n\n\tconst handleFileChange = useCallback(\n\t\t(e: ChangeEvent<HTMLInputElement>) => {\n\t\t\tif (e.target.files && e.target.files.length > 0) {\n\t\t\t\taddFiles(e.target.files)\n\t\t\t}\n\t\t},\n\t\t[addFiles],\n\t)\n\n\tconst openFileDialog = useCallback(() => {\n\t\tif (inputRef.current) {\n\t\t\tinputRef.current.click()\n\t\t}\n\t}, [])\n\n\tconst getInputProps = useCallback(\n\t\t(props: InputHTMLAttributes<HTMLInputElement> = {}) => {\n\t\t\treturn {\n\t\t\t\t...props,\n\t\t\t\ttype: 'file' as const,\n\t\t\t\tonChange: handleFileChange,\n\t\t\t\taccept: props.accept || accept,\n\t\t\t\tmultiple: props.multiple !== undefined ? props.multiple : multiple,\n\t\t\t\tref: inputRef,\n\t\t\t}\n\t\t},\n\t\t[accept, multiple, handleFileChange],\n\t)\n\n\treturn [\n\t\tstate,\n\t\t{\n\t\t\taddFiles,\n\t\t\tremoveFile,\n\t\t\tclearFiles,\n\t\t\tclearErrors,\n\t\t\thandleDragEnter,\n\t\t\thandleDragLeave,\n\t\t\thandleDragOver,\n\t\t\thandleDrop,\n\t\t\thandleFileChange,\n\t\t\topenFileDialog,\n\t\t\tgetInputProps,\n\t\t},\n\t]\n}\n\n// Helper function to format bytes to human-readable format\nexport const formatBytes = (bytes: number, decimals = 2): string => {\n\tif (bytes === 0) return '0 Bytes'\n\n\tconst k = 1024\n\tconst dm = decimals < 0 ? 0 : decimals\n\tconst sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n\n\tconst i = Math.floor(Math.log(bytes) / Math.log(k))\n\n\treturn Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}