shadimage
FormCn
componentfile-uploadsource: ok · refreshed

File Input

File upload component

npx shadcn add "https://formcn.dev/r/file-upload.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light

Registry dependencies

https://formcn.dev/r/use-file-upload.jsonbutton

Files1 · source cached 10/8/2026, 2:11:14 AM

src/components/form-fields/file-upload.tsx
'use client'

import { AlertCircleIcon, CloudUpload, File, XIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
	formatBytes,
	useFileUpload,
} from '@/form-builder/hooks/use-file-upload'

const getFileIcon = (file: { file: File | { type: string; name: string } }) => {
	const fileType = file.file.type
	if (fileType.startsWith('image/')) {
		// return preview
		return (
			<div className="aspect-square size-10 overflow-hidden rounded-md">
				<img
					src={URL.createObjectURL(file.file as Blob)}
					className="object-fill"
					alt="File preview"
				/>
			</div>
		)
	}
	return (
		<div className="aspect-square size-10 flex items-center justify-center overflow-hidden rounded-full">
			<File className="size-5 opacity-60" />
		</div>
	)
}

export function FileUpload({
	maxFiles,
	maxSize,
	placeholder,
	// description,
	required,
	setValue,
	accept,
	name,
	disabled,
}: {
	maxFiles: number
	maxSize: number
	placeholder?: string
	// description?: string;
	required?: boolean
	disabled?: boolean
	setValue: (
		name: string,
		value: any,
		options?: {
			shouldValidate?: boolean
			shouldDirty?: boolean
			shouldTouch?: boolean
		},
	) => void
	accept?: string
	name: string
}) {
	const [
		{ files, isDragging, errors },
		{
			handleDragEnter,
			handleDragLeave,
			handleDragOver,
			handleDrop,
			openFileDialog,
			removeFile,
			clearFiles,
			getInputProps,
		},
	] = useFileUpload({
		multiple: maxFiles > 1,
		maxFiles,
		maxSize,
		accept,
		onFilesChange: (files) => {
			setValue(
				name,
				files.map((file) => file.file),
				{ shouldValidate: true, shouldDirty: true, shouldTouch: true },
			)
		},
	})

	return (
		<div className="flex flex-col gap-2 pb-2">
			{/* Drop area */}
			<div
				role="button"
				onClick={openFileDialog}
				onDragEnter={handleDragEnter}
				onDragLeave={handleDragLeave}
				onDragOver={handleDragOver}
				onDrop={handleDrop}
				data-dragging={isDragging || undefined}
				className="border-input hover:bg-accent/50 data-[dragging=true]:bg-accent/50 has-[input:focus]:border-ring has-[input:focus]:ring-ring/50 flex min-h-32 flex-col items-center justify-center rounded-md border border-dashed p-4 transition-colors has-disabled:pointer-events-none has-disabled:opacity-50 has-[input:focus]:ring-[3px] hover:cursor-pointer"
			>
				{/* @ts-expect-error caused when upgrading to Nextjs 16 */}
				<input
					{...getInputProps()}
					className="sr-only"
					aria-label="Upload files"
					required={required}
					disabled={disabled}
				/>

				<div className="flex flex-col items-center justify-center text-center">
					<div
						className="bg-secondary mb-2 flex size-11 shrink-0 items-center justify-center rounded-full border"
						aria-hidden="true"
					>
						<CloudUpload className="size-4 opacity-60" />
					</div>
					{/* <p className="mb-1.5 text-sm font-medium">
            Upload
          </p> */}
					<p className="text-foreground font-medium text-sm mb-2">
						Drag & drop or click to browse
					</p>
					<div className="text-muted-foreground/70 flex flex-wrap justify-center gap-1 text-xs">
						{placeholder}
					</div>
				</div>
			</div>

			{errors.length > 0 && (
				<div
					className="text-destructive flex items-center gap-1 text-xs"
					role="alert"
				>
					<AlertCircleIcon className="size-3 shrink-0" />
					<span>{errors[0]}</span>
				</div>
			)}

			{/* File list */}
			{files.length > 0 && (
				<div className="space-y-2">
					{files.map((file) => (
						<div
							key={file.id}
							className="flex items-center justify-between gap-2 rounded-lg border p-2 pe-3"
						>
							<div className="flex items-center gap-1.5 overflow-hidden">
								{getFileIcon(file)}
								<div className="flex min-w-0 flex-col gap-0.5">
									<p className="truncate text-[11px] font-medium max-w-[200px]">
										{file.file.name}
									</p>
									<p className="text-muted-foreground text-xs">
										{formatBytes(file.file.size)}
									</p>
								</div>
							</div>

							<Button
								size="icon"
								variant="ghost"
								className="text-muted-foreground/80 hover:text-foreground -me-2 size-8 hover:bg-transparent"
								onClick={() => removeFile(file.id)}
								aria-label="Remove file"
							>
								<XIcon className="size-4" aria-hidden="true" />
							</Button>
						</div>
					))}

					{/* Remove all files button */}
					{files.length > 1 && (
						<div className="flex justify-end">
							<Button size="sm" variant="outline" onClick={clearFiles}>
								Remove all files
							</Button>
						</div>
					)}
				</div>
			)}
		</div>
	)
}

More from FormCn

Browse all components in this library →