Smart File Drop
An intelligent file upload zone with drag-and-drop physics, file type detection, and simulated progress states. Provides rich visual feedback for every interaction.
Installation
Add this component to your project using the CLI:
terminal
npx vui-registry-cli-v1 add smart-file-dropSource Code
smart-file-drop.tsx
'use client'
import React, { useState, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { UploadCloud, File, FileImage, FileText, X, Check, AlertCircle, Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils'
// @ts-ignore
import confetti from 'canvas-confetti'
type FileStatus = 'idle' | 'dragging' | 'uploading' | 'success' | 'error'
interface SmartFileDropProps {
onUpload?: (file: File) => Promise<void> | void;
acceptedFileTypes?: string[];
maxSize?: number; // in MB
}
export default function SmartFileDrop({
onUpload,
acceptedFileTypes,
maxSize = 5
}: SmartFileDropProps) {
return (
<div className="flex items-center justify-center min-h-[500px] bg-neutral-100 dark:bg-neutral-950 p-8">
<FileDropZone
onUpload={onUpload}
acceptedFileTypes={acceptedFileTypes}
maxSize={maxSize}
/>
</div>
)
}
function FileDropZone({
onUpload,
acceptedFileTypes,
maxSize
}: SmartFileDropProps) {
const [status, setStatus] = useState<FileStatus>('idle')
const [file, setFile] = useState<File | null>(null)
const [progress, setProgress] = useState(0)
const inputRef = useRef<HTMLInputElement>(null)
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
if (status === 'idle' || status === 'error') setStatus('dragging')
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
if (status === 'dragging') setStatus('idle')
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
if (status === 'uploading' || status === 'success') return
const droppedFile = e.dataTransfer.files[0]
if (droppedFile) processFile(droppedFile)
}
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
processFile(e.target.files[0])
}
}
const processFile = async (file: File) => {
setFile(file)
setStatus('uploading')
// Simulate upload if no onUpload provided
if (!onUpload) {
let p = 0
const interval = setInterval(() => {
p += Math.random() * 10
if (p > 100) {
p = 100
clearInterval(interval)
setStatus('success')
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
})
}
setProgress(p)
}, 200)
} else {
try {
await onUpload(file);
setStatus('success');
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
})
} catch (e) {
setStatus('error');
}
}
}
const reset = (e: React.MouseEvent) => {
e.stopPropagation()
setStatus('idle')
setFile(null)
setProgress(0)
if (inputRef.current) inputRef.current.value = ''
}
const getIcon = () => {
if (!file) return <UploadCloud size={48} className="text-neutral-400" />
if (file.type.startsWith('image/')) return <FileImage size={48} className="text-purple-500" />
if (file.type.includes('pdf')) return <FileText size={48} className="text-red-500" />
return <File size={48} className="text-blue-500" />
}
return (
<motion.div
layout
className={cn(
"relative w-full max-w-lg aspect-video rounded-[2rem] border-2 border-dashed transition-all duration-500 flex flex-col items-center justify-center p-8 cursor-pointer overflow-hidden bg-white/80 dark:bg-neutral-900/80 backdrop-blur-xl shadow-2xl",
status === 'dragging'
? "border-blue-500 bg-blue-50/50 dark:bg-blue-900/20 scale-[1.02] shadow-blue-500/20"
: "border-neutral-300 dark:border-white/10 hover:border-neutral-400 dark:hover:border-white/20 hover:bg-white dark:hover:bg-neutral-900",
status === 'success' && "border-green-500 bg-green-50/50 dark:bg-green-900/20",
status === 'error' && "border-red-500 bg-red-50/50 dark:bg-red-900/20"
)}
onClick={() => inputRef.current?.click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<input
ref={inputRef}
type="file"
className="hidden"
onChange={handleFileSelect}
accept={acceptedFileTypes?.join(',')}
/>
<AnimatePresence mode="wait">
{status === 'idle' || status === 'dragging' ? (
<motion.div
key="idle"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex flex-col items-center text-center gap-4"
>
<div className={cn(
"w-20 h-20 rounded-full bg-neutral-100 dark:bg-white/5 flex items-center justify-center transition-transform duration-500",
status === 'dragging' && "scale-110 bg-blue-100 dark:bg-blue-500/20"
)}>
<UploadCloud
size={32}
className={cn(
"text-neutral-400 transition-colors duration-500",
status === 'dragging' && "text-blue-500"
)}
/>
</div>
<div>
<p className="text-lg font-semibold text-neutral-700 dark:text-neutral-200">
{status === 'dragging' ? "Drop it like it's hot!" : "Click or drag file to upload"}
</p>
<p className="text-sm text-neutral-400 mt-1">
SVG, PNG, JPG or GIF (max {maxSize}MB)
</p>
</div>
</motion.div>
) : (
<motion.div
key="active"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center gap-6 w-full max-w-xs"
>
<div className="relative">
{status === 'uploading' && (
<motion.div
className="absolute inset-0 rounded-full border-4 border-neutral-200 dark:border-neutral-800"
style={{ borderTopColor: 'var(--blue-500)', rotate: 360 }}
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
/>
)}
<div className="w-24 h-24 rounded-2xl bg-white dark:bg-neutral-800 shadow-xl flex items-center justify-center relative z-10">
{getIcon()}
{status === 'success' && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute -right-2 -bottom-2 w-8 h-8 bg-green-500 rounded-full flex items-center justify-center border-4 border-white dark:border-neutral-900"
>
<Check size={16} className="text-white" strokeWidth={3} />
</motion.div>
)}
</div>
</div>
<div className="w-full text-center">
<p className="font-medium text-neutral-900 dark:text-white truncate px-4">
{file?.name}
</p>
<p className="text-sm text-neutral-500 mt-1">
{(file?.size || 0) > 1024 * 1024
? `${((file?.size || 0) / (1024 * 1024)).toFixed(1)} MB`
: `${((file?.size || 0) / 1024).toFixed(0)} KB`}
</p>
</div>
{status === 'uploading' && (
<div className="w-full h-2 bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-500"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
/>
</div>
)}
{status === 'success' && (
<motion.button
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
onClick={reset}
className="px-6 py-2 rounded-full bg-neutral-900 dark:bg-white text-white dark:text-black font-medium text-sm hover:scale-105 transition-transform"
>
Upload Another
</motion.button>
)}
{status === 'error' && (
<div className="flex items-center gap-2 text-red-500">
<AlertCircle size={16} />
<span className="text-sm font-medium">Upload failed</span>
<button onClick={reset} className="ml-2 underline text-xs">Try again</button>
</div>
)}
</motion.div>
)}
</AnimatePresence>
</motion.div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latestcanvas-confetti: latest@types/canvas-confetti: latest

