"use client"; import { Slot } from "@radix-ui/react-slot"; import { FileArchiveIcon, FileAudioIcon, FileCodeIcon, FileCogIcon, FileIcon, FileTextIcon, FileVideoIcon, } from "lucide-react"; import { type ChangeEvent, type ComponentPropsWithoutRef, createContext, type DragEvent, forwardRef, type KeyboardEvent, type MouseEvent, type ReactNode, type RefObject, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from "react"; import { cn } from "~/lib/utils"; const ROOT_NAME = "FileUpload"; const DROPZONE_NAME = "FileUploadDropzone"; const TRIGGER_NAME = "FileUploadTrigger"; const LIST_NAME = "FileUploadList"; const ITEM_NAME = "FileUploadItem"; const ITEM_PREVIEW_NAME = "FileUploadItemPreview"; const ITEM_METADATA_NAME = "FileUploadItemMetadata"; const ITEM_PROGRESS_NAME = "FileUploadItemProgress"; const ITEM_DELETE_NAME = "FileUploadItemDelete"; const CLEAR_NAME = "FileUploadClear"; const FILE_UPLOAD_ERRORS = { [ROOT_NAME]: `\`${ROOT_NAME}\` must be used as root component`, [DROPZONE_NAME]: `\`${DROPZONE_NAME}\` must be within \`${ROOT_NAME}\``, [TRIGGER_NAME]: `\`${TRIGGER_NAME}\` must be within \`${ROOT_NAME}\``, [LIST_NAME]: `\`${LIST_NAME}\` must be within \`${ROOT_NAME}\``, [ITEM_NAME]: `\`${ITEM_NAME}\` must be within \`${ROOT_NAME}\``, [ITEM_PREVIEW_NAME]: `\`${ITEM_PREVIEW_NAME}\` must be within \`${ITEM_NAME}\``, [ITEM_METADATA_NAME]: `\`${ITEM_METADATA_NAME}\` must be within \`${ITEM_NAME}\``, [ITEM_PROGRESS_NAME]: `\`${ITEM_PROGRESS_NAME}\` must be within \`${ITEM_NAME}\``, [ITEM_DELETE_NAME]: `\`${ITEM_DELETE_NAME}\` must be within \`${ITEM_NAME}\``, [CLEAR_NAME]: `\`${CLEAR_NAME}\` must be within \`${ROOT_NAME}\``, } as const; const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; function useAsRef(data: T) { const ref = useRef(data); useIsomorphicLayoutEffect(() => { ref.current = data; }); return ref; } function useLazyRef(fn: () => T) { const ref = useRef(null); if (ref.current === null) { ref.current = fn(); } return ref as RefObject; } type Direction = "ltr" | "rtl"; const DirectionContext = createContext(undefined); function useDirection(dirProp?: Direction): Direction { const contextDir = useContext(DirectionContext); return dirProp ?? contextDir ?? "ltr"; } interface FileState { file: File; progress: number; error?: string; status: "idle" | "uploading" | "error" | "success"; } interface StoreState { files: Map; dragOver: boolean; invalid: boolean; } type StoreAction = | { variant: "ADD_FILES"; files: File[] } | { variant: "SET_FILES"; files: File[] } | { variant: "SET_PROGRESS"; file: File; progress: number } | { variant: "SET_SUCCESS"; file: File } | { variant: "SET_ERROR"; file: File; error: string } | { variant: "REMOVE_FILE"; file: File } | { variant: "SET_DRAG_OVER"; dragOver: boolean } | { variant: "SET_INVALID"; invalid: boolean } | { variant: "CLEAR" }; function createStore( listeners: Set<() => void>, files: Map, onValueChange?: (files: File[]) => void, invalid?: boolean, ) { const initialState: StoreState = { dragOver: false, files, invalid: invalid ?? false, }; let state = initialState; function reducer(state: StoreState, action: StoreAction): StoreState { switch (action.variant) { case "ADD_FILES": { for (const file of action.files) { files.set(file, { file, progress: 0, status: "idle", }); } if (onValueChange) { const fileList = Array.from(files.values()).map( (fileState) => fileState.file, ); onValueChange(fileList); } return { ...state, files }; } case "SET_FILES": { const newFileSet = new Set(action.files); for (const existingFile of files.keys()) { if (!newFileSet.has(existingFile)) { files.delete(existingFile); } } for (const file of action.files) { const existingState = files.get(file); if (!existingState) { files.set(file, { file, progress: 0, status: "idle", }); } } return { ...state, files }; } case "SET_PROGRESS": { const fileState = files.get(action.file); if (fileState) { files.set(action.file, { ...fileState, progress: action.progress, status: "uploading", }); } return { ...state, files }; } case "SET_SUCCESS": { const fileState = files.get(action.file); if (fileState) { files.set(action.file, { ...fileState, progress: 100, status: "success", }); } return { ...state, files }; } case "SET_ERROR": { const fileState = files.get(action.file); if (fileState) { files.set(action.file, { ...fileState, error: action.error, status: "error", }); } return { ...state, files }; } case "REMOVE_FILE": { files.delete(action.file); if (onValueChange) { const fileList = Array.from(files.values()).map( (fileState) => fileState.file, ); onValueChange(fileList); } return { ...state, files }; } case "SET_DRAG_OVER": { return { ...state, dragOver: action.dragOver }; } case "SET_INVALID": { return { ...state, invalid: action.invalid }; } case "CLEAR": { files.clear(); if (onValueChange) { onValueChange([]); } return { ...state, files, invalid: false }; } default: return state; } } function getState() { return state; } function dispatch(action: StoreAction) { state = reducer(state, action); for (const listener of listeners) { listener(); } } function subscribe(listener: () => void) { listeners.add(listener); return () => listeners.delete(listener); } return { dispatch, getState, subscribe }; } const StoreContext = createContext | null>(null); StoreContext.displayName = ROOT_NAME; function useStoreContext(name: keyof typeof FILE_UPLOAD_ERRORS) { const context = useContext(StoreContext); if (!context) { throw new Error(FILE_UPLOAD_ERRORS[name]); } return context; } function useStore(selector: (state: StoreState) => T): T { const store = useStoreContext(ROOT_NAME); const lastValueRef = useLazyRef<{ value: T; state: StoreState } | null>( () => null, ); const getSnapshot = useCallback(() => { const state = store.getState(); const prevValue = lastValueRef.current; if (prevValue && prevValue.state === state) { return prevValue.value; } const nextValue = selector(state); lastValueRef.current = { state, value: nextValue }; return nextValue; }, [store, selector, lastValueRef]); return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot); } interface FileUploadContextValue { inputId: string; dropzoneId: string; listId: string; labelId: string; disabled: boolean; dir: Direction; inputRef: RefObject; } const FileUploadContext = createContext(null); function useFileUploadContext(name: keyof typeof FILE_UPLOAD_ERRORS) { const context = useContext(FileUploadContext); if (!context) { throw new Error(FILE_UPLOAD_ERRORS[name]); } return context; } interface FileUploadRootProps extends Omit, "defaultValue" | "onChange"> { value?: File[]; defaultValue?: File[]; onValueChange?: (files: File[]) => void; onAccept?: (files: File[]) => void; onFileAccept?: (file: File) => void; onFileReject?: (file: File, message: string) => void; onFileValidate?: (file: File) => string | null | undefined; onUpload?: ( files: File[], options: { onProgress: (file: File, progress: number) => void; onSuccess: (file: File) => void; onError: (file: File, error: Error) => void; }, ) => Promise | void; accept?: string; maxFiles?: number; maxSize?: number; dir?: Direction; label?: string; name?: string; asChild?: boolean; disabled?: boolean; invalid?: boolean; multiple?: boolean; required?: boolean; } const FileUploadRoot = forwardRef( (props, forwardedRef) => { const { value, defaultValue, onValueChange, //onAccept, //onFileAccept, //onFileReject, //onFileValidate, //onUpload, accept, //maxFiles, //maxSize, dir: dirProp, label, name, asChild, disabled = false, invalid = false, multiple = false, required = false, children, className, ...rootProps } = props; const inputId = useId(); const dropzoneId = useId(); const listId = useId(); const labelId = useId(); const dir = useDirection(dirProp); const propsRef = useAsRef(props); const listeners = useLazyRef(() => new Set<() => void>()).current; const files = useLazyRef>(() => new Map()).current; const inputRef = useRef(null); const isControlled = value !== undefined; const store = useMemo( () => createStore(listeners, files, onValueChange, invalid), [listeners, files, onValueChange, invalid], ); const contextValue = useMemo( () => ({ dir, disabled, dropzoneId, inputId, inputRef, labelId, listId, }), [dropzoneId, inputId, listId, labelId, dir, disabled], ); useEffect(() => { if (isControlled) { store.dispatch({ files: value, variant: "SET_FILES" }); } else if ( defaultValue && defaultValue.length > 0 && !store.getState().files.size ) { store.dispatch({ files: defaultValue, variant: "SET_FILES" }); } }, [value, defaultValue, isControlled, store]); const onFilesChange = useCallback( (originalFiles: File[]) => { if (propsRef.current.disabled) return; let filesToProcess = [...originalFiles]; let invalid = false; if (propsRef.current.maxFiles) { const currentCount = store.getState().files.size; const remainingSlotCount = Math.max( 0, propsRef.current.maxFiles - currentCount, ); if (remainingSlotCount < filesToProcess.length) { const rejectedFiles = filesToProcess.slice(remainingSlotCount); invalid = true; filesToProcess = filesToProcess.slice(0, remainingSlotCount); for (const file of rejectedFiles) { let rejectionMessage = `Maximum ${propsRef.current.maxFiles} files allowed`; if (propsRef.current.onFileValidate) { const validationMessage = propsRef.current.onFileValidate(file); if (validationMessage) { rejectionMessage = validationMessage; } } propsRef.current.onFileReject?.(file, rejectionMessage); } } } const acceptedFiles: File[] = []; const rejectedFiles: { file: File; message: string }[] = []; for (const file of filesToProcess) { let rejected = false; let rejectionMessage = ""; if (propsRef.current.onFileValidate) { const validationMessage = propsRef.current.onFileValidate(file); if (validationMessage) { rejectionMessage = validationMessage; propsRef.current.onFileReject?.(file, rejectionMessage); rejected = true; invalid = true; continue; } } if (propsRef.current.accept) { const acceptTypes = propsRef.current.accept .split(",") .map((t) => t.trim()); const fileType = file.type; const fileExtension = `.${file.name.split(".").pop()}`; if ( !acceptTypes.some( (type) => type === fileType || type === fileExtension || (type.includes("/*") && fileType.startsWith(type.replace("/*", "/"))), ) ) { rejectionMessage = "File type not accepted"; propsRef.current.onFileReject?.(file, rejectionMessage); rejected = true; invalid = true; } } if ( propsRef.current.maxSize && file.size > propsRef.current.maxSize ) { rejectionMessage = "File too large"; propsRef.current.onFileReject?.(file, rejectionMessage); rejected = true; invalid = true; } if (!rejected) { acceptedFiles.push(file); } else { rejectedFiles.push({ file, message: rejectionMessage }); } } if (invalid) { store.dispatch({ invalid, variant: "SET_INVALID" }); setTimeout(() => { store.dispatch({ invalid: false, variant: "SET_INVALID" }); }, 2000); } if (acceptedFiles.length > 0) { store.dispatch({ files: acceptedFiles, variant: "ADD_FILES" }); if (isControlled && propsRef.current.onValueChange) { const currentFiles = Array.from( store.getState().files.values(), ).map((f) => f.file); propsRef.current.onValueChange([...currentFiles]); } if (propsRef.current.onAccept) { propsRef.current.onAccept(acceptedFiles); } for (const file of acceptedFiles) { propsRef.current.onFileAccept?.(file); } if (propsRef.current.onUpload) { requestAnimationFrame(() => { onFilesUpload(acceptedFiles).catch((e) => { throw new Error((e as Error).message); }); }); } } }, [store, isControlled, propsRef], ); const onFilesUpload = useCallback( async (files: File[]) => { try { for (const file of files) { store.dispatch({ file, progress: 0, variant: "SET_PROGRESS" }); } if (propsRef.current.onUpload) { await propsRef.current.onUpload(files, { onError: (file, error) => { store.dispatch({ error: error.message ?? "Upload failed", file, variant: "SET_ERROR", }); }, onProgress: (file, progress) => { store.dispatch({ file, progress: Math.min(Math.max(0, progress), 100), variant: "SET_PROGRESS", }); }, onSuccess: (file) => { store.dispatch({ file, variant: "SET_SUCCESS" }); }, }); } else { for (const file of files) { store.dispatch({ file, variant: "SET_SUCCESS" }); } } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Upload failed"; for (const file of files) { store.dispatch({ error: errorMessage, file, variant: "SET_ERROR", }); } } }, [store, propsRef.current.onUpload], ); const onInputChange = useCallback( (event: ChangeEvent) => { const files = Array.from(event.target.files ?? []); onFilesChange(files); event.target.value = ""; }, [onFilesChange], ); const RootPrimitive = asChild ? Slot : "div"; return ( {children} {label ?? "File upload"} ); }, ); FileUploadRoot.displayName = ROOT_NAME; interface FileUploadDropzoneProps extends ComponentPropsWithoutRef<"div"> { asChild?: boolean; } const FileUploadDropzone = forwardRef( (props, forwardedRef) => { const { asChild, className, ...dropzoneProps } = props; const context = useFileUploadContext(DROPZONE_NAME); const store = useStoreContext(DROPZONE_NAME); const dragOver = useStore((state) => state.dragOver); const invalid = useStore((state) => state.invalid); const propsRef = useAsRef(dropzoneProps); const onClick = useCallback( (event: MouseEvent) => { propsRef.current?.onClick?.(event); if (event.defaultPrevented) return; const target = event.target; const isFromTrigger = target instanceof HTMLElement && target.closest('[data-slot="file-upload-trigger"]'); if (!isFromTrigger) { context.inputRef.current?.click(); } }, [context.inputRef, propsRef], ); const onDragOver = useCallback( (event: DragEvent) => { propsRef.current?.onDragOver?.(event); if (event.defaultPrevented) return; event.preventDefault(); store.dispatch({ dragOver: true, variant: "SET_DRAG_OVER" }); }, [store, propsRef.current.onDragOver], ); const onDragEnter = useCallback( (event: DragEvent) => { propsRef.current?.onDragEnter?.(event); if (event.defaultPrevented) return; event.preventDefault(); store.dispatch({ dragOver: true, variant: "SET_DRAG_OVER" }); }, [store, propsRef.current.onDragEnter], ); const onDragLeave = useCallback( (event: DragEvent) => { propsRef.current?.onDragLeave?.(event); if (event.defaultPrevented) return; event.preventDefault(); store.dispatch({ dragOver: false, variant: "SET_DRAG_OVER" }); }, [store, propsRef.current.onDragLeave], ); const onDrop = useCallback( (event: DragEvent) => { propsRef.current?.onDrop?.(event); if (event.defaultPrevented) return; event.preventDefault(); store.dispatch({ dragOver: false, variant: "SET_DRAG_OVER" }); const files = Array.from(event.dataTransfer.files); const inputElement = context.inputRef.current; if (!inputElement) return; const dataTransfer = new DataTransfer(); for (const file of files) { dataTransfer.items.add(file); } inputElement.files = dataTransfer.files; inputElement.dispatchEvent(new Event("change", { bubbles: true })); }, [store, context.inputRef, propsRef.current.onDrop], ); const onKeyDown = useCallback( (event: KeyboardEvent) => { propsRef.current?.onKeyDown?.(event); if ( !event.defaultPrevented && (event.key === "Enter" || event.key === " ") ) { event.preventDefault(); context.inputRef.current?.click(); } }, [context.inputRef, propsRef.current.onKeyDown], ); const DropzonePrimitive = asChild ? Slot : "div"; return ( ); }, ); FileUploadDropzone.displayName = DROPZONE_NAME; interface FileUploadTriggerProps extends ComponentPropsWithoutRef<"button"> { asChild?: boolean; } const FileUploadTrigger = forwardRef( (props, forwardedRef) => { const { asChild, ...triggerProps } = props; const context = useFileUploadContext(TRIGGER_NAME); const propsRef = useAsRef(triggerProps); const onClick = useCallback( (event: MouseEvent) => { propsRef.current?.onClick?.(event); if (event.defaultPrevented) return; context.inputRef.current?.click(); }, [context.inputRef, propsRef.current], ); const TriggerPrimitive = asChild ? Slot : "button"; return ( ); }, ); FileUploadTrigger.displayName = TRIGGER_NAME; interface FileUploadListProps extends ComponentPropsWithoutRef<"div"> { orientation?: "horizontal" | "vertical"; asChild?: boolean; forceMount?: boolean; } const FileUploadList = forwardRef( (props, forwardedRef) => { const { className, orientation = "vertical", asChild, forceMount, ...listProps } = props; const context = useFileUploadContext(LIST_NAME); const shouldRender = forceMount || useStore((state) => state.files.size > 0); if (!shouldRender) return null; const ListPrimitive = asChild ? Slot : "div"; return ( ); }, ); FileUploadList.displayName = LIST_NAME; interface FileUploadItemContextValue { id: string; fileState: FileState | undefined; nameId: string; sizeId: string; statusId: string; messageId: string; } const FileUploadItemContext = createContext( null, ); function useFileUploadItemContext(name: keyof typeof FILE_UPLOAD_ERRORS) { const context = useContext(FileUploadItemContext); if (!context) { throw new Error(FILE_UPLOAD_ERRORS[name]); } return context; } interface FileUploadItemProps extends ComponentPropsWithoutRef<"div"> { value: File; asChild?: boolean; } const FileUploadItem = forwardRef( (props, forwardedRef) => { const { value, asChild, className, ...itemProps } = props; const id = useId(); const statusId = `${id}-status`; const nameId = `${id}-name`; const sizeId = `${id}-size`; const messageId = `${id}-message`; const context = useFileUploadContext(ITEM_NAME); const fileState = useStore((state) => state.files.get(value)); const fileCount = useStore((state) => state.files.size); const fileIndex = useStore((state) => { const files = Array.from(state.files.keys()); return files.indexOf(value) + 1; }); const itemContext = useMemo( () => ({ fileState, id, messageId, nameId, sizeId, statusId, }), [id, fileState, statusId, nameId, sizeId, messageId], ); if (!fileState) return null; const statusText = (() => { if (fileState.error) return `Error: ${fileState.error}`; if (fileState.status === "uploading") return `Uploading: ${fileState.progress}% complete`; if (fileState.status === "success") return "Upload complete"; return "Ready to upload"; })(); const ItemPrimitive = asChild ? Slot : "div"; return ( {props.children} {statusText} ); }, ); FileUploadItem.displayName = ITEM_NAME; function formatBytes(bytes: number) { if (bytes === 0) return "0 B"; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)} ${sizes[i]}`; } function getFileIcon(file: File) { const type = file.type; const extension = file.name.split(".").pop()?.toLowerCase() ?? ""; if (type.startsWith("video/")) { return ; } if (type.startsWith("audio/")) { return ; } if ( type.startsWith("text/") || ["txt", "md", "rtf", "pdf"].includes(extension) ) { return ; } if ( [ "html", "css", "js", "jsx", "ts", "tsx", "json", "xml", "php", "py", "rb", "java", "c", "cpp", "cs", ].includes(extension) ) { return ; } if (["zip", "rar", "7z", "tar", "gz", "bz2"].includes(extension)) { return ; } if ( ["exe", "msi", "app", "apk", "deb", "rpm"].includes(extension) || type.startsWith("application/") ) { return ; } return ; } interface FileUploadItemPreviewProps extends ComponentPropsWithoutRef<"div"> { render?: (file: File) => ReactNode; asChild?: boolean; } const FileUploadItemPreview = forwardRef< HTMLDivElement, FileUploadItemPreviewProps >((props, forwardedRef) => { const { render, asChild, children, className, ...previewProps } = props; const itemContext = useFileUploadItemContext(ITEM_PREVIEW_NAME); const isImage = itemContext.fileState?.file.type.startsWith("image/"); const onPreviewRender = useCallback( (file: File) => { if (render) return render(file); if (isImage) { return ( // biome-ignore lint/a11y/noNoninteractiveElementInteractions: {file.name} { if (!(event.target instanceof HTMLImageElement)) return; URL.revokeObjectURL(event.target.src); }} src={URL.createObjectURL(file)} /> ); } return getFileIcon(file); }, [isImage, render], ); if (!itemContext.fileState) return null; const ItemPreviewPrimitive = asChild ? Slot : "div"; return ( svg]:size-7", className, )} ref={forwardedRef} > {onPreviewRender(itemContext.fileState.file)} {children} ); }); FileUploadItemPreview.displayName = ITEM_PREVIEW_NAME; interface FileUploadItemMetadataProps extends ComponentPropsWithoutRef<"div"> { asChild?: boolean; } const FileUploadItemMetadata = forwardRef< HTMLDivElement, FileUploadItemMetadataProps >((props, forwardedRef) => { const { asChild, children, className, ...metadataProps } = props; const context = useFileUploadContext(ITEM_METADATA_NAME); const itemContext = useFileUploadItemContext(ITEM_METADATA_NAME); if (!itemContext.fileState) return null; const ItemMetadataPrimitive = asChild ? Slot : "div"; return ( {children ?? ( <> {itemContext.fileState.file.name} {formatBytes(itemContext.fileState.file.size)} {itemContext.fileState.error && ( {itemContext.fileState.error} )} )} ); }); FileUploadItemMetadata.displayName = ITEM_METADATA_NAME; interface FileUploadItemProgressProps extends ComponentPropsWithoutRef<"div"> { asChild?: boolean; circular?: boolean; size?: number; } const FileUploadItemProgress = forwardRef< HTMLDivElement, FileUploadItemProgressProps >((props, forwardedRef) => { const { circular, size = 40, asChild, className, ...progressProps } = props; const itemContext = useFileUploadItemContext(ITEM_PROGRESS_NAME); if (!itemContext.fileState) return null; const ItemProgressPrimitive = asChild ? Slot : "div"; if (circular) { if (itemContext.fileState.status === "success") return null; const circumference = 2 * Math.PI * ((size - 4) / 2); const strokeDashoffset = circumference - (itemContext.fileState.progress / 100) * circumference; return ( Progress ); } return (
); }); FileUploadItemProgress.displayName = ITEM_PROGRESS_NAME; interface FileUploadItemDeleteProps extends ComponentPropsWithoutRef<"button"> { asChild?: boolean; } const FileUploadItemDelete = forwardRef< HTMLButtonElement, FileUploadItemDeleteProps >((props, forwardedRef) => { const { asChild, ...deleteProps } = props; const store = useStoreContext(ITEM_DELETE_NAME); const itemContext = useFileUploadItemContext(ITEM_DELETE_NAME); const propsRef = useAsRef(deleteProps); const onClick = useCallback( (event: MouseEvent) => { propsRef.current?.onClick?.(event); if (!itemContext.fileState || event.defaultPrevented) return; store.dispatch({ file: itemContext.fileState.file, variant: "REMOVE_FILE", }); }, [store, itemContext.fileState, propsRef.current?.onClick], ); if (!itemContext.fileState) return null; const ItemDeletePrimitive = asChild ? Slot : "button"; return ( ); }); FileUploadItemDelete.displayName = ITEM_DELETE_NAME; interface FileUploadClearProps extends ComponentPropsWithoutRef<"button"> { forceMount?: boolean; asChild?: boolean; } const FileUploadClear = forwardRef( (props, forwardedRef) => { const { asChild, forceMount, disabled, ...clearProps } = props; const context = useFileUploadContext(CLEAR_NAME); const store = useStoreContext(CLEAR_NAME); const propsRef = useAsRef(clearProps); const isDisabled = disabled || context.disabled; const onClick = useCallback( (event: MouseEvent) => { propsRef.current?.onClick?.(event); if (event.defaultPrevented) return; store.dispatch({ variant: "CLEAR" }); }, [store, propsRef], ); const shouldRender = forceMount || useStore((state) => state.files.size > 0); if (!shouldRender) return null; const ClearPrimitive = asChild ? Slot : "button"; return ( ); }, ); FileUploadClear.displayName = CLEAR_NAME; const FileUpload = FileUploadRoot; // const Root = FileUploadRoot; // const Trigger = FileUploadTrigger; // const Dropzone = FileUploadDropzone; // const List = FileUploadList; // const Item = FileUploadItem; // const ItemPreview = FileUploadItemPreview; // const ItemMetadata = FileUploadItemMetadata; // const ItemProgress = FileUploadItemProgress; // const ItemDelete = FileUploadItemDelete; // const Clear = FileUploadClear; export { FileUpload, FileUploadDropzone, FileUploadTrigger, FileUploadList, FileUploadItem, FileUploadItemPreview, FileUploadItemMetadata, FileUploadItemProgress, FileUploadItemDelete, //FileUploadClear, /* Root, Dropzone, Trigger, List, Item, ItemPreview, ItemMetadata, ItemProgress, ItemDelete, Clear, useStore as useFileUpload,*/ };