File Upload Dropzone
A file upload component with drag-and-drop support, progress simulation, and file previews.
Installation
Add this component to your project using the CLI:
npx vui-registry-cli-v1 add file-uploadSource Code
'use client'
import React, { useState, useRef, useCallback } from 'react'
import { motion, AnimatePresence, useAnimation } from 'framer-motion'
import { Upload, X, File, FileImage, FileText, CheckCircle2, AlertCircle, Trash2 } from 'lucide-react'
import { cn } from '@/lib/utils'
interface FileUploadProps {
onUpload?: (files: File[]) => void
maxFiles?: number
accept?: string[] // e.g., ['image/png', 'application/pdf']
className?: string
}
interface FileStatus {
file: File
id: string
progress: number
status: 'uploading' | 'completed' | 'error'
preview?: string
}
export function FileUpload({ onUpload, maxFiles = 5, accept, className }: FileUploadProps) {
const [files, setFiles] = useState<FileStatus[]>([])
const [isDragging, setIsDragging] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const controls = useAnimation()
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
controls.start({ scale: 1.02, borderColor: 'var(--primary)' })
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
controls.start({ scale: 1, borderColor: 'var(--border)' })
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
}
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
controls.start({ scale: 1, borderColor: 'var(--border)' })
const droppedFiles = Array.from(e.dataTransfer.files)
handleFiles(droppedFiles)
},
[maxFiles, accept],
)
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const selectedFiles = Array.from(e.target.files)
handleFiles(selectedFiles)
}
}
const handleFiles = (newFiles: File[]) => {
const validFiles = newFiles.slice(0, maxFiles - files.length)
const newFileStatuses: FileStatus[] = validFiles.map((file) => ({
file,
id: Math.random().toString(36).substring(7),
progress: 0,
status: 'uploading',
preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,
}))
setFiles((prev) => [...prev, ...newFileStatuses])
// Simulate upload progress
newFileStatuses.forEach((fileStatus) => {
simulateUpload(fileStatus.id)
})
if (onUpload) {
onUpload(validFiles)
}
}
const simulateUpload = (id: string) => {
let progress = 0
const interval = setInterval(() => {
progress += Math.random() * 10 + 5
if (progress >= 100) {
progress = 100
clearInterval(interval)
setFiles((prev) =>
prev.map((f) => (f.id === id ? { ...f, progress: 100, status: 'completed' } : f)),
)
} else {
setFiles((prev) =>
prev.map((f) => (f.id === id ? { ...f, progress } : f)),
)
}
}, 200)
}
const removeFile = (id: string) => {
setFiles((prev) => prev.filter((f) => f.id !== id))
}
const getFileIcon = (file: File) => {
if (file.type.startsWith('image/')) return <FileImage className="w-5 h-5 text-blue-500" />
if (file.type === 'application/pdf') return <FileText className="w-5 h-5 text-red-500" />
return <File className="w-5 h-5 text-gray-500" />
}
return (
<div className={cn('w-full max-w-xl mx-auto font-sans', className)}>
<motion.div
animate={controls}
initial={{ scale: 1, borderColor: 'var(--border)' }}
className={cn(
'relative overflow-hidden rounded-2xl border-2 border-dashed transition-colors duration-300',
isDragging
? 'border-blue-500/50 bg-blue-500/5'
: 'border-foreground/10 bg-foreground/2 hover:bg-foreground/4',
)}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<div className="flex flex-col items-center justify-center py-12 px-6 text-center cursor-pointer">
<div className="relative mb-4">
<motion.div
className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl"
animate={{ scale: isDragging ? 1.5 : 1 }}
/>
<div className="relative bg-background p-4 rounded-full border border-foreground/10 shadow-sm">
<Upload className="w-8 h-8 text-foreground/70" />
</div>
</div>
<h3 className="text-lg font-semibold text-foreground mb-1">
Drag files to upload
</h3>
<p className="text-sm text-muted-foreground mb-4">
or click to select files
</p>
<div className="flex gap-2 text-xs text-muted-foreground/60">
<span>Supported formats: PNG, JPG, PDF</span>
<span>•</span>
<span>Max size: 10MB</span>
</div>
</div>
<input
ref={fileInputRef}
type="file"
multiple
className="hidden"
onChange={handleFileInput}
accept={accept?.join(',')}
/>
{/* Decorative Grid */}
<div className="absolute inset-0 pointer-events-none opacity-[0.03]"
style={{
backgroundImage: 'radial-gradient(#000 1px, transparent 1px)',
backgroundSize: '20px 20px'
}}
/>
</motion.div>
<div className="mt-6 space-y-3">
<AnimatePresence mode='popLayout'>
{files.map((fileStatus) => (
<motion.div
key={fileStatus.id}
layout
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, x: -10, scale: 0.95 }}
className="group relative flex items-center gap-4 p-3 rounded-xl border border-foreground/10 bg-background/50 backdrop-blur-sm hover:bg-background/80 transition-colors"
>
<div className="relative w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 border border-foreground/5 flex items-center justify-center bg-foreground/5">
{fileStatus.preview ? (
<img src={fileStatus.preview} alt={fileStatus.file.name} className="w-full h-full object-cover" />
) : (
getFileIcon(fileStatus.file)
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<p className="text-sm font-medium text-foreground truncate max-w-[200px]">
{fileStatus.file.name}
</p>
<span className="text-xs text-muted-foreground tabular-nums">
{(fileStatus.file.size / 1024 / 1024).toFixed(2)} MB
</span>
</div>
<div className="relative h-1.5 w-full bg-foreground/5 rounded-full overflow-hidden">
<motion.div
className={cn(
"absolute top-0 left-0 h-full rounded-full transition-colors duration-300",
fileStatus.status === 'completed' ? "bg-green-500" : "bg-blue-500"
)}
initial={{ width: 0 }}
animate={{ width: `${fileStatus.progress}%` }}
transition={{ ease: "easeOut" }}
/>
</div>
</div>
<div className="flex items-center gap-2">
{fileStatus.status === 'completed' && (
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }}>
<CheckCircle2 className="w-5 h-5 text-green-500" />
</motion.div>
)}
{fileStatus.status === 'uploading' && (
<span className="text-xs text-blue-500 font-medium">{Math.round(fileStatus.progress)}%</span>
)}
<button
onClick={() => removeFile(fileStatus.id)}
className="p-1.5 rounded-full hover:bg-red-500/10 hover:text-red-500 text-muted-foreground transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| onUpload | (files: File[]) => void | undefined | Callback with uploaded files. |
| maxFiles | number | 5 | Maximum number of files allowed. |
| accept | string[] | undefined | Accepted MIME types (e.g., ['image/png']). |
| className | string | undefined | Additional CSS classes. |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

