(\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
\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 } 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"
]
}