{ "name": "file-upload", "type": "registry:ui", "files": [ { "path": "ui/file-upload.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {\n FileArchiveIcon,\n FileAudioIcon,\n FileCodeIcon,\n FileCogIcon,\n FileIcon,\n FileTextIcon,\n FileVideoIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/base/hooks/use-as-ref\";\nimport { useLazyRef } from \"@/registry/bases/base/hooks/use-lazy-ref\";\nimport { useDirection } from \"@/registry/bases/base/ui/direction\";\n\nconst ROOT_NAME = \"FileUpload\";\nconst DROPZONE_NAME = \"FileUploadDropzone\";\nconst TRIGGER_NAME = \"FileUploadTrigger\";\nconst LIST_NAME = \"FileUploadList\";\nconst ITEM_NAME = \"FileUploadItem\";\nconst ITEM_PREVIEW_NAME = \"FileUploadItemPreview\";\nconst ITEM_METADATA_NAME = \"FileUploadItemMetadata\";\nconst ITEM_PROGRESS_NAME = \"FileUploadItemProgress\";\nconst ITEM_DELETE_NAME = \"FileUploadItemDelete\";\nconst CLEAR_NAME = \"FileUploadClear\";\n\nfunction formatBytes(bytes: number) {\n if (bytes === 0) return \"0 B\";\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)} ${sizes[i]}`;\n}\n\nfunction getFileIcon(file: File) {\n const type = file.type;\n const extension = file.name.split(\".\").pop()?.toLowerCase() ?? \"\";\n\n if (type.startsWith(\"video/\")) {\n return ;\n }\n\n if (type.startsWith(\"audio/\")) {\n return ;\n }\n\n if (\n type.startsWith(\"text/\") ||\n [\"txt\", \"md\", \"rtf\", \"pdf\"].includes(extension)\n ) {\n return ;\n }\n\n if (\n [\n \"html\",\n \"css\",\n \"js\",\n \"jsx\",\n \"ts\",\n \"tsx\",\n \"json\",\n \"xml\",\n \"php\",\n \"py\",\n \"rb\",\n \"java\",\n \"c\",\n \"cpp\",\n \"cs\",\n ].includes(extension)\n ) {\n return ;\n }\n\n if ([\"zip\", \"rar\", \"7z\", \"tar\", \"gz\", \"bz2\"].includes(extension)) {\n return ;\n }\n\n if (\n [\"exe\", \"msi\", \"app\", \"apk\", \"deb\", \"rpm\"].includes(extension) ||\n type.startsWith(\"application/\")\n ) {\n return ;\n }\n\n return ;\n}\n\ntype Direction = \"ltr\" | \"rtl\";\n\ninterface FileState {\n file: File;\n progress: number;\n error?: string;\n status: \"idle\" | \"uploading\" | \"error\" | \"success\";\n}\n\ninterface StoreState {\n files: Map;\n dragOver: boolean;\n invalid: boolean;\n}\n\ntype StoreAction =\n | { type: \"ADD_FILES\"; files: File[] }\n | { type: \"SET_FILES\"; files: File[] }\n | { type: \"SET_PROGRESS\"; file: File; progress: number }\n | { type: \"SET_SUCCESS\"; file: File }\n | { type: \"SET_ERROR\"; file: File; error: string }\n | { type: \"REMOVE_FILE\"; file: File }\n | { type: \"SET_DRAG_OVER\"; dragOver: boolean }\n | { type: \"SET_INVALID\"; invalid: boolean }\n | { type: \"CLEAR\" };\n\ntype Store = {\n getState: () => StoreState;\n dispatch: (action: StoreAction) => void;\n subscribe: (listener: () => void) => () => void;\n};\n\nconst StoreContext = React.createContext(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\nfunction useStore(selector: (state: StoreState) => T): T {\n const store = useStoreContext(\"useStore\");\n\n const lastValueRef = useLazyRef<{ value: T; state: StoreState } | null>(\n () => null,\n );\n\n const getSnapshot = React.useCallback(() => {\n const state = store.getState();\n const prevValue = lastValueRef.current;\n\n if (prevValue && prevValue.state === state) {\n return prevValue.value;\n }\n\n const nextValue = selector(state);\n lastValueRef.current = { value: nextValue, state };\n return nextValue;\n }, [store, selector, lastValueRef]);\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface FileUploadContextValue {\n inputId: string;\n dropzoneId: string;\n listId: string;\n labelId: string;\n disabled: boolean;\n dir: Direction;\n inputRef: React.RefObject;\n urlCache: WeakMap;\n}\n\nconst FileUploadContext = React.createContext(\n null,\n);\n\nfunction useFileUploadContext(consumerName: string) {\n const context = React.useContext(FileUploadContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface FileUploadProps\n extends Omit<\n React.ComponentProps<\"div\"> & useRender.ComponentProps<\"div\">,\n \"defaultValue\" | \"onChange\"\n > {\n value?: File[];\n defaultValue?: File[];\n onValueChange?: (files: File[]) => void;\n onAccept?: (files: File[]) => void;\n onFileAccept?: (file: File) => void;\n onFileReject?: (file: File, message: string) => void;\n onFileValidate?: (file: File) => string | null | undefined;\n onUpload?: (\n files: File[],\n options: {\n onProgress: (file: File, progress: number) => void;\n onSuccess: (file: File) => void;\n onError: (file: File, error: Error) => void;\n },\n ) => Promise | void;\n accept?: string;\n maxFiles?: number;\n maxSize?: number;\n dir?: Direction;\n label?: string;\n name?: string;\n disabled?: boolean;\n invalid?: boolean;\n multiple?: boolean;\n required?: boolean;\n}\n\nfunction FileUpload(props: FileUploadProps) {\n const {\n value,\n defaultValue,\n onValueChange,\n onAccept,\n onFileAccept,\n onFileReject,\n onFileValidate,\n onUpload,\n accept,\n maxFiles,\n maxSize,\n dir: dirProp,\n label,\n name,\n render,\n disabled = false,\n invalid = false,\n multiple = false,\n required = false,\n children,\n className,\n ...rootProps\n } = props;\n\n const inputId = React.useId();\n const dropzoneId = React.useId();\n const listId = React.useId();\n const labelId = React.useId();\n\n const contextDir = useDirection();\n const dir = dirProp ?? contextDir;\n const listeners = useLazyRef(() => new Set<() => void>()).current;\n const files = useLazyRef>(() => new Map()).current;\n const urlCache = useLazyRef(() => new WeakMap()).current;\n const inputRef = React.useRef(null);\n const isControlled = value !== undefined;\n\n const propsRef = useAsRef({\n onValueChange,\n onAccept,\n onFileAccept,\n onFileReject,\n onFileValidate,\n onUpload,\n });\n\n const store = React.useMemo(() => {\n let state: StoreState = {\n files,\n dragOver: false,\n invalid: invalid,\n };\n\n function reducer(state: StoreState, action: StoreAction): StoreState {\n switch (action.type) {\n case \"ADD_FILES\": {\n for (const file of action.files) {\n files.set(file, {\n file,\n progress: 0,\n status: \"idle\",\n });\n }\n\n if (propsRef.current.onValueChange) {\n const fileList = Array.from(files.values()).map(\n (fileState) => fileState.file,\n );\n propsRef.current.onValueChange(fileList);\n }\n return { ...state, files };\n }\n\n case \"SET_FILES\": {\n const newFileSet = new Set(action.files);\n for (const existingFile of files.keys()) {\n if (!newFileSet.has(existingFile)) {\n files.delete(existingFile);\n }\n }\n\n for (const file of action.files) {\n const existingState = files.get(file);\n if (!existingState) {\n files.set(file, {\n file,\n progress: 0,\n status: \"idle\",\n });\n }\n }\n return { ...state, files };\n }\n\n case \"SET_PROGRESS\": {\n const fileState = files.get(action.file);\n if (fileState) {\n files.set(action.file, {\n ...fileState,\n progress: action.progress,\n status: \"uploading\",\n });\n }\n return { ...state, files };\n }\n\n case \"SET_SUCCESS\": {\n const fileState = files.get(action.file);\n if (fileState) {\n files.set(action.file, {\n ...fileState,\n progress: 100,\n status: \"success\",\n });\n }\n return { ...state, files };\n }\n\n case \"SET_ERROR\": {\n const fileState = files.get(action.file);\n if (fileState) {\n files.set(action.file, {\n ...fileState,\n error: action.error,\n status: \"error\",\n });\n }\n return { ...state, files };\n }\n\n case \"REMOVE_FILE\": {\n const cachedUrl = urlCache.get(action.file);\n if (cachedUrl) {\n URL.revokeObjectURL(cachedUrl);\n urlCache.delete(action.file);\n }\n\n files.delete(action.file);\n\n if (propsRef.current.onValueChange) {\n const fileList = Array.from(files.values()).map(\n (fileState) => fileState.file,\n );\n propsRef.current.onValueChange(fileList);\n }\n return { ...state, files };\n }\n\n case \"SET_DRAG_OVER\": {\n return { ...state, dragOver: action.dragOver };\n }\n\n case \"SET_INVALID\": {\n return { ...state, invalid: action.invalid };\n }\n\n case \"CLEAR\": {\n for (const file of files.keys()) {\n const cachedUrl = urlCache.get(file);\n if (cachedUrl) {\n URL.revokeObjectURL(cachedUrl);\n urlCache.delete(file);\n }\n }\n\n files.clear();\n if (propsRef.current.onValueChange) {\n propsRef.current.onValueChange([]);\n }\n return { ...state, files, invalid: false };\n }\n\n default:\n return state;\n }\n }\n\n return {\n getState: () => state,\n dispatch: (action) => {\n state = reducer(state, action);\n for (const listener of listeners) {\n listener();\n }\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n }, [listeners, files, invalid, propsRef, urlCache]);\n\n const acceptTypes = React.useMemo(\n () => accept?.split(\",\").map((t) => t.trim()) ?? null,\n [accept],\n );\n\n const onProgress = useLazyRef(() => {\n let frame = 0;\n return (file: File, progress: number) => {\n if (frame) return;\n frame = requestAnimationFrame(() => {\n frame = 0;\n store.dispatch({\n type: \"SET_PROGRESS\",\n file,\n progress: Math.min(Math.max(0, progress), 100),\n });\n });\n };\n }).current;\n\n React.useEffect(() => {\n if (isControlled) {\n store.dispatch({ type: \"SET_FILES\", files: value });\n } else if (\n defaultValue &&\n defaultValue.length > 0 &&\n !store.getState().files.size\n ) {\n store.dispatch({ type: \"SET_FILES\", files: defaultValue });\n }\n }, [value, defaultValue, isControlled, store]);\n\n React.useEffect(() => {\n return () => {\n for (const file of files.keys()) {\n const cachedUrl = urlCache.get(file);\n if (cachedUrl) {\n URL.revokeObjectURL(cachedUrl);\n }\n }\n };\n }, [files, urlCache]);\n\n const onFilesUpload = React.useCallback(\n async (files: File[]) => {\n try {\n for (const file of files) {\n store.dispatch({ type: \"SET_PROGRESS\", file, progress: 0 });\n }\n\n if (propsRef.current.onUpload) {\n await propsRef.current.onUpload(files, {\n onProgress,\n onSuccess: (file) => {\n store.dispatch({ type: \"SET_SUCCESS\", file });\n },\n onError: (file, error) => {\n store.dispatch({\n type: \"SET_ERROR\",\n file,\n error: error.message ?? \"Upload failed\",\n });\n },\n });\n } else {\n for (const file of files) {\n store.dispatch({ type: \"SET_SUCCESS\", file });\n }\n }\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : \"Upload failed\";\n for (const file of files) {\n store.dispatch({\n type: \"SET_ERROR\",\n file,\n error: errorMessage,\n });\n }\n }\n },\n [store, propsRef, onProgress],\n );\n\n const onFilesChange = React.useCallback(\n (originalFiles: File[]) => {\n if (disabled) return;\n\n let filesToProcess = [...originalFiles];\n let invalid = false;\n\n if (maxFiles) {\n const currentCount = store.getState().files.size;\n const remainingSlotCount = Math.max(0, maxFiles - currentCount);\n\n if (remainingSlotCount < filesToProcess.length) {\n const rejectedFiles = filesToProcess.slice(remainingSlotCount);\n invalid = true;\n\n filesToProcess = filesToProcess.slice(0, remainingSlotCount);\n\n for (const file of rejectedFiles) {\n let rejectionMessage = `Maximum ${maxFiles} files allowed`;\n\n if (propsRef.current.onFileValidate) {\n const validationMessage = propsRef.current.onFileValidate(file);\n if (validationMessage) {\n rejectionMessage = validationMessage;\n }\n }\n\n propsRef.current.onFileReject?.(file, rejectionMessage);\n }\n }\n }\n\n const acceptedFiles: File[] = [];\n const rejectedFiles: { file: File; message: string }[] = [];\n\n for (const file of filesToProcess) {\n let rejected = false;\n let rejectionMessage = \"\";\n\n if (propsRef.current.onFileValidate) {\n const validationMessage = propsRef.current.onFileValidate(file);\n if (validationMessage) {\n rejectionMessage = validationMessage;\n propsRef.current.onFileReject?.(file, rejectionMessage);\n rejected = true;\n invalid = true;\n continue;\n }\n }\n\n if (acceptTypes) {\n const fileType = file.type;\n const fileExtension = `.${file.name.split(\".\").pop()}`;\n\n if (\n !acceptTypes.some(\n (type) =>\n type === fileType ||\n type === fileExtension ||\n (type.includes(\"/*\") &&\n fileType.startsWith(type.replace(\"/*\", \"/\"))),\n )\n ) {\n rejectionMessage = \"File type not accepted\";\n propsRef.current.onFileReject?.(file, rejectionMessage);\n rejected = true;\n invalid = true;\n }\n }\n\n if (maxSize && file.size > maxSize) {\n rejectionMessage = \"File too large\";\n propsRef.current.onFileReject?.(file, rejectionMessage);\n rejected = true;\n invalid = true;\n }\n\n if (!rejected) {\n acceptedFiles.push(file);\n } else {\n rejectedFiles.push({ file, message: rejectionMessage });\n }\n }\n\n if (invalid) {\n store.dispatch({ type: \"SET_INVALID\", invalid });\n setTimeout(() => {\n store.dispatch({ type: \"SET_INVALID\", invalid: false });\n }, 2000);\n }\n\n if (acceptedFiles.length > 0) {\n store.dispatch({ type: \"ADD_FILES\", files: acceptedFiles });\n\n if (isControlled && propsRef.current.onValueChange) {\n const currentFiles = Array.from(store.getState().files.values()).map(\n (f) => f.file,\n );\n propsRef.current.onValueChange([...currentFiles]);\n }\n\n if (propsRef.current.onAccept) {\n propsRef.current.onAccept(acceptedFiles);\n }\n\n for (const file of acceptedFiles) {\n propsRef.current.onFileAccept?.(file);\n }\n\n if (propsRef.current.onUpload) {\n requestAnimationFrame(() => {\n onFilesUpload(acceptedFiles);\n });\n }\n }\n },\n [\n store,\n isControlled,\n propsRef,\n onFilesUpload,\n maxFiles,\n acceptTypes,\n maxSize,\n disabled,\n ],\n );\n\n const onInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const files = Array.from(event.target.files ?? []);\n onFilesChange(files);\n event.target.value = \"\";\n },\n [onFilesChange],\n );\n\n const contextValue = React.useMemo(\n () => ({\n dropzoneId,\n inputId,\n listId,\n labelId,\n dir,\n disabled,\n inputRef,\n urlCache,\n }),\n [dropzoneId, inputId, listId, labelId, dir, disabled, urlCache],\n );\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n dir,\n className: cn(\"relative flex flex-col gap-2\", className),\n children: (\n <>\n {children}\n \n
\n {label ?? \"File upload\"}\n
\n \n ),\n },\n rootProps,\n ),\n render,\n state: {\n slot: \"file-upload\",\n disabled: disabled ? \"\" : undefined,\n },\n });\n\n return (\n \n \n {element}\n \n \n );\n}\n\ninterface FileUploadDropzoneProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\nfunction FileUploadDropzone(props: FileUploadDropzoneProps) {\n const {\n render,\n className,\n onClick: onClickProp,\n onDragOver: onDragOverProp,\n onDragEnter: onDragEnterProp,\n onDragLeave: onDragLeaveProp,\n onDrop: onDropProp,\n onPaste: onPasteProp,\n onKeyDown: onKeyDownProp,\n ...dropzoneProps\n } = props;\n\n const context = useFileUploadContext(DROPZONE_NAME);\n const store = useStoreContext(DROPZONE_NAME);\n const dragOver = useStore((state) => state.dragOver);\n const invalid = useStore((state) => state.invalid);\n\n const propsRef = useAsRef({\n onClick: onClickProp,\n onDragOver: onDragOverProp,\n onDragEnter: onDragEnterProp,\n onDragLeave: onDragLeaveProp,\n onDrop: onDropProp,\n onPaste: onPasteProp,\n onKeyDown: onKeyDownProp,\n });\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n const target = event.target;\n\n const isFromTrigger =\n target instanceof HTMLElement &&\n target.closest('[data-slot=\"file-upload-trigger\"]');\n\n if (!isFromTrigger) {\n context.inputRef.current?.click();\n }\n },\n [context.inputRef, propsRef],\n );\n\n const onDragOver = React.useCallback(\n (event: React.DragEvent) => {\n propsRef.current.onDragOver?.(event);\n\n if (event.defaultPrevented) return;\n\n event.preventDefault();\n store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: true });\n },\n [store, propsRef],\n );\n\n const onDragEnter = React.useCallback(\n (event: React.DragEvent) => {\n propsRef.current.onDragEnter?.(event);\n\n if (event.defaultPrevented) return;\n\n event.preventDefault();\n store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: true });\n },\n [store, propsRef],\n );\n\n const onDragLeave = React.useCallback(\n (event: React.DragEvent) => {\n propsRef.current.onDragLeave?.(event);\n\n if (event.defaultPrevented) return;\n\n const relatedTarget = event.relatedTarget;\n if (\n relatedTarget &&\n relatedTarget instanceof Node &&\n event.currentTarget.contains(relatedTarget)\n ) {\n return;\n }\n\n event.preventDefault();\n store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: false });\n },\n [store, propsRef],\n );\n\n const onDrop = React.useCallback(\n (event: React.DragEvent) => {\n propsRef.current.onDrop?.(event);\n\n if (event.defaultPrevented) return;\n\n event.preventDefault();\n store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: false });\n\n const files = Array.from(event.dataTransfer.files);\n const inputElement = context.inputRef.current;\n if (!inputElement) return;\n\n const dataTransfer = new DataTransfer();\n for (const file of files) {\n dataTransfer.items.add(file);\n }\n\n inputElement.files = dataTransfer.files;\n inputElement.dispatchEvent(new Event(\"change\", { bubbles: true }));\n },\n [store, context.inputRef, propsRef],\n );\n\n const onPaste = React.useCallback(\n (event: React.ClipboardEvent) => {\n propsRef.current.onPaste?.(event);\n\n if (event.defaultPrevented) return;\n\n event.preventDefault();\n store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: false });\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n const files: File[] = [];\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n if (item?.kind === \"file\") {\n const file = item.getAsFile();\n if (file) {\n files.push(file);\n }\n }\n }\n\n if (files.length === 0) return;\n\n const inputElement = context.inputRef.current;\n if (!inputElement) return;\n\n const dataTransfer = new DataTransfer();\n for (const file of files) {\n dataTransfer.items.add(file);\n }\n\n inputElement.files = dataTransfer.files;\n inputElement.dispatchEvent(new Event(\"change\", { bubbles: true }));\n },\n [store, context.inputRef, propsRef],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n propsRef.current.onKeyDown?.(event);\n\n if (\n !event.defaultPrevented &&\n (event.key === \"Enter\" || event.key === \" \")\n ) {\n event.preventDefault();\n context.inputRef.current?.click();\n }\n },\n [context.inputRef, propsRef],\n );\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"region\",\n id: context.dropzoneId,\n \"aria-controls\": `${context.inputId} ${context.listId}`,\n \"aria-disabled\": context.disabled,\n \"aria-invalid\": invalid,\n dir: context.dir,\n tabIndex: context.disabled ? undefined : 0,\n className: cn(\n \"relative flex select-none flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 outline-none transition-colors hover:bg-accent/30 focus-visible:border-ring/50 data-disabled:pointer-events-none data-dragging:border-primary/30 data-invalid:border-destructive data-dragging:bg-accent/30 data-invalid:ring-destructive/20\",\n className,\n ),\n onClick,\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n onKeyDown,\n onPaste,\n },\n dropzoneProps,\n ),\n render,\n state: {\n slot: \"file-upload-dropzone\",\n disabled: context.disabled ? \"\" : undefined,\n dragging: dragOver ? \"\" : undefined,\n invalid: invalid ? \"\" : undefined,\n },\n });\n}\n\ninterface FileUploadTriggerProps\n extends React.ComponentProps<\"button\">,\n useRender.ComponentProps<\"button\"> {}\n\nfunction FileUploadTrigger(props: FileUploadTriggerProps) {\n const { render, onClick: onClickProp, ...triggerProps } = props;\n\n const context = useFileUploadContext(TRIGGER_NAME);\n\n const propsRef = useAsRef({\n onClick: onClickProp,\n });\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n context.inputRef.current?.click();\n },\n [context.inputRef, propsRef],\n );\n\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\n type: \"button\",\n \"aria-controls\": context.inputId,\n disabled: context.disabled,\n onClick,\n },\n triggerProps,\n ),\n render,\n state: {\n slot: \"file-upload-trigger\",\n disabled: context.disabled ? \"\" : undefined,\n },\n });\n}\n\ninterface FileUploadListProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n orientation?: \"horizontal\" | \"vertical\";\n forceMount?: boolean;\n}\n\nfunction FileUploadList(props: FileUploadListProps) {\n const {\n className,\n orientation = \"vertical\",\n render,\n forceMount,\n ...listProps\n } = props;\n\n const context = useFileUploadContext(LIST_NAME);\n const fileCount = useStore((state) => state.files.size);\n const shouldRender = forceMount || fileCount > 0;\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"list\",\n id: context.listId,\n \"aria-orientation\": orientation,\n dir: context.dir,\n className: cn(\n \"data-[state=inactive]:fade-out-0 data-[state=active]:fade-in-0 data-[state=inactive]:slide-out-to-top-2 data-[state=active]:slide-in-from-top-2 flex flex-col gap-2 data-[state=active]:animate-in data-[state=inactive]:animate-out\",\n orientation === \"horizontal\" && \"flex-row overflow-x-auto p-1.5\",\n className,\n ),\n },\n listProps,\n ),\n render,\n state: {\n slot: \"file-upload-list\",\n orientation,\n state: shouldRender ? \"active\" : \"inactive\",\n },\n });\n\n if (!shouldRender) return null;\n\n return element;\n}\n\ninterface FileUploadItemContextValue {\n id: string;\n fileState: FileState | undefined;\n nameId: string;\n sizeId: string;\n statusId: string;\n messageId: string;\n}\n\nconst FileUploadItemContext =\n React.createContext(null);\n\nfunction useFileUploadItemContext(consumerName: string) {\n const context = React.useContext(FileUploadItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n }\n return context;\n}\n\ninterface FileUploadItemProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n value: File;\n}\n\nfunction FileUploadItem(props: FileUploadItemProps) {\n const { value, render, className, ...itemProps } = props;\n\n const id = React.useId();\n const statusId = `${id}-status`;\n const nameId = `${id}-name`;\n const sizeId = `${id}-size`;\n const messageId = `${id}-message`;\n\n const context = useFileUploadContext(ITEM_NAME);\n const fileState = useStore((state) => state.files.get(value));\n const fileCount = useStore((state) => state.files.size);\n const fileIndex = useStore((state) => {\n const files = Array.from(state.files.keys());\n return files.indexOf(value) + 1;\n });\n\n const itemContext = React.useMemo(\n () => ({\n id,\n fileState,\n nameId,\n sizeId,\n statusId,\n messageId,\n }),\n [id, fileState, statusId, nameId, sizeId, messageId],\n );\n\n const statusText = fileState?.error\n ? `Error: ${fileState.error}`\n : fileState?.status === \"uploading\"\n ? `Uploading: ${fileState.progress}% complete`\n : fileState?.status === \"success\"\n ? \"Upload complete\"\n : \"Ready to upload\";\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"listitem\",\n id,\n \"aria-setsize\": fileCount,\n \"aria-posinset\": fileIndex,\n \"aria-describedby\": `${nameId} ${sizeId} ${statusId} ${\n fileState?.error ? messageId : \"\"\n }`,\n \"aria-labelledby\": nameId,\n dir: context.dir,\n className: cn(\n \"relative flex items-center gap-2.5 rounded-md border p-3\",\n className,\n ),\n children: (\n <>\n {props.children}\n \n {statusText}\n \n \n ),\n },\n itemProps,\n ),\n render,\n state: {\n slot: \"file-upload-item\",\n },\n });\n\n if (!fileState) return null;\n\n return (\n \n {element}\n \n );\n}\n\ninterface FileUploadItemPreviewProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n previewRender?: (\n file: File,\n fallback: () => React.ReactNode,\n ) => React.ReactNode;\n}\n\nfunction FileUploadItemPreview(props: FileUploadItemPreviewProps) {\n const { previewRender, render, children, className, ...previewProps } = props;\n\n const itemContext = useFileUploadItemContext(ITEM_PREVIEW_NAME);\n const context = useFileUploadContext(ITEM_PREVIEW_NAME);\n\n const getDefaultRender = React.useCallback(\n (file: File) => {\n if (itemContext.fileState?.file.type.startsWith(\"image/\")) {\n let url = context.urlCache.get(file);\n if (!url) {\n url = URL.createObjectURL(file);\n context.urlCache.set(file, url);\n }\n\n return (\n // biome-ignore lint/performance/noImgElement: dynamic file URLs from user uploads don't work well with Next.js Image optimization\n {file.name}\n );\n }\n\n return getFileIcon(file);\n },\n [itemContext.fileState?.file.type, context.urlCache],\n );\n\n const onPreviewRender = React.useCallback(\n (file: File) => {\n if (previewRender) {\n return previewRender(file, () => getDefaultRender(file));\n }\n\n return getDefaultRender(file);\n },\n [previewRender, getDefaultRender],\n );\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n \"aria-labelledby\": itemContext.nameId,\n className: cn(\n \"relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded border bg-accent/50 [&>svg]:size-10\",\n className,\n ),\n children: itemContext.fileState ? (\n <>\n {onPreviewRender(itemContext.fileState.file)}\n {children}\n \n ) : null,\n },\n previewProps,\n ),\n render,\n state: {\n slot: \"file-upload-preview\",\n },\n });\n\n if (!itemContext.fileState) return null;\n\n return element;\n}\n\ninterface FileUploadItemMetadataProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n size?: \"default\" | \"sm\";\n}\n\nfunction FileUploadItemMetadata(props: FileUploadItemMetadataProps) {\n const {\n render,\n size = \"default\",\n children,\n className,\n ...metadataProps\n } = props;\n\n const context = useFileUploadContext(ITEM_METADATA_NAME);\n const itemContext = useFileUploadItemContext(ITEM_METADATA_NAME);\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n dir: context.dir,\n className: cn(\"flex min-w-0 flex-1 flex-col\", className),\n children: children ?? (\n <>\n \n {itemContext.fileState?.file.name}\n \n \n {itemContext.fileState\n ? formatBytes(itemContext.fileState.file.size)\n : \"\"}\n \n {itemContext.fileState?.error && (\n \n {itemContext.fileState.error}\n \n )}\n \n ),\n },\n metadataProps,\n ),\n render,\n state: {\n slot: \"file-upload-metadata\",\n },\n });\n\n if (!itemContext.fileState) return null;\n\n return element;\n}\ninterface FileUploadItemProgressProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n variant?: \"linear\" | \"circular\" | \"fill\";\n size?: number;\n forceMount?: boolean;\n}\n\nfunction FileUploadItemProgress(props: FileUploadItemProgressProps) {\n const {\n variant = \"linear\",\n size = 40,\n render,\n forceMount,\n className,\n ...progressProps\n } = props;\n\n const itemContext = useFileUploadItemContext(ITEM_PROGRESS_NAME);\n\n const shouldRender =\n forceMount ||\n (itemContext.fileState?.progress !== 100 &&\n itemContext.fileState?.progress !== undefined);\n\n let elementProps: React.ComponentProps<\"div\"> & {\n children?: React.ReactNode;\n };\n\n if (variant === \"circular\") {\n const circumference = 2 * Math.PI * ((size - 4) / 2);\n const strokeDashoffset = itemContext.fileState\n ? circumference - (itemContext.fileState.progress / 100) * circumference\n : circumference;\n\n elementProps = {\n role: \"progressbar\",\n \"aria-valuemin\": 0,\n \"aria-valuemax\": 100,\n \"aria-valuenow\": itemContext.fileState?.progress ?? 0,\n \"aria-valuetext\": `${itemContext.fileState?.progress ?? 0}%`,\n \"aria-labelledby\": itemContext.nameId,\n className: cn(\n \"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2\",\n className,\n ),\n children: (\n \n \n \n \n ),\n };\n } else if (variant === \"fill\") {\n const progressPercentage = itemContext.fileState?.progress ?? 0;\n const topInset = 100 - progressPercentage;\n\n elementProps = {\n role: \"progressbar\",\n \"aria-valuemin\": 0,\n \"aria-valuemax\": 100,\n \"aria-valuenow\": progressPercentage,\n \"aria-valuetext\": `${progressPercentage}%`,\n \"aria-labelledby\": itemContext.nameId,\n className: cn(\n \"absolute inset-0 bg-primary/50 transition-[clip-path] duration-300 ease-linear\",\n className,\n ),\n style: {\n clipPath: `inset(${topInset}% 0% 0% 0%)`,\n },\n };\n } else {\n elementProps = {\n role: \"progressbar\",\n \"aria-valuemin\": 0,\n \"aria-valuemax\": 100,\n \"aria-valuenow\": itemContext.fileState?.progress ?? 0,\n \"aria-valuetext\": `${itemContext.fileState?.progress ?? 0}%`,\n \"aria-labelledby\": itemContext.nameId,\n className: cn(\n \"relative h-1.5 w-full overflow-hidden rounded-full bg-primary/20\",\n className,\n ),\n children: (\n \n ),\n };\n }\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(elementProps, progressProps),\n render,\n state: {\n slot: \"file-upload-progress\",\n variant,\n },\n });\n\n if (!itemContext.fileState || !shouldRender) return null;\n\n return element;\n}\n\ninterface FileUploadItemDeleteProps\n extends React.ComponentProps<\"button\">,\n useRender.ComponentProps<\"button\"> {}\n\nfunction FileUploadItemDelete(props: FileUploadItemDeleteProps) {\n const { render, onClick: onClickProp, ...deleteProps } = props;\n\n const store = useStoreContext(ITEM_DELETE_NAME);\n const itemContext = useFileUploadItemContext(ITEM_DELETE_NAME);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n onClickProp?.(event);\n\n if (!itemContext.fileState || event.defaultPrevented) return;\n\n store.dispatch({\n type: \"REMOVE_FILE\",\n file: itemContext.fileState.file,\n });\n },\n [store, itemContext.fileState, onClickProp],\n );\n\n const element = useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\n type: \"button\",\n \"aria-controls\": itemContext.id,\n \"aria-describedby\": itemContext.nameId,\n onClick,\n },\n deleteProps,\n ),\n render,\n state: {\n slot: \"file-upload-item-delete\",\n },\n });\n\n if (!itemContext.fileState) return null;\n\n return element;\n}\n\ninterface FileUploadClearProps\n extends React.ComponentProps<\"button\">,\n useRender.ComponentProps<\"button\"> {\n forceMount?: boolean;\n}\n\nfunction FileUploadClear(props: FileUploadClearProps) {\n const {\n render,\n forceMount,\n disabled,\n onClick: onClickProp,\n ...clearProps\n } = props;\n\n const context = useFileUploadContext(CLEAR_NAME);\n const store = useStoreContext(CLEAR_NAME);\n const fileCount = useStore((state) => state.files.size);\n\n const isDisabled = disabled || context.disabled;\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n onClickProp?.(event);\n\n if (event.defaultPrevented) return;\n\n store.dispatch({ type: \"CLEAR\" });\n },\n [store, onClickProp],\n );\n\n const shouldRender = forceMount || fileCount > 0;\n\n const element = useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\n type: \"button\",\n \"aria-controls\": context.listId,\n disabled: isDisabled,\n onClick,\n },\n clearProps,\n ),\n render,\n state: {\n slot: \"file-upload-clear\",\n disabled: isDisabled ? \"\" : undefined,\n },\n });\n\n if (!shouldRender) return null;\n\n return element;\n}\n\nexport {\n FileUpload,\n FileUploadClear,\n FileUploadDropzone,\n FileUploadItem,\n FileUploadItemDelete,\n FileUploadItemMetadata,\n FileUploadItemPreview,\n FileUploadItemProgress,\n FileUploadList,\n type FileUploadProps,\n FileUploadTrigger,\n useStore as useFileUpload,\n};\n", "target": "" } ], "registryDependencies": [ "direction", "@diceui/use-as-ref", "@diceui/use-lazy-ref" ], "dependencies": [ "@base-ui/react" ] }