File Upload
src/components/thegridcn/file-upload.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface FileUploadProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onDrop"> {
accept?: string;
description?: string;
disabled?: boolean;
label?: string;
maxSize?: number;
multiple?: boolean;
onFiles?: (files: File[]) => void;
}
export function FileUpload({
accept,
multiple = false,
maxSize,
onFiles,
label = "DROP FILES HERE",
description = "or click to browse",
disabled = false,
className,
...props
}: FileUploadProps) {
const [dragging, setDragging] = React.useState(false);
const [files, setFiles] = React.useState<File[]>([]);
const inputRef = React.useRef<HTMLInputElement>(null);
function handleFiles(fileList: FileList) {
const arr = Array.from(fileList);
const filtered = maxSize ? arr.filter((f) => f.size <= maxSize) : arr;
setFiles(filtered);
onFiles?.(filtered);
}
function handleDrop(e: React.DragEvent) {
e.preventDefault();
setDragging(false);
if (disabled || !e.dataTransfer.files.length) {
return;
}
handleFiles(e.dataTransfer.files);
}
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
if (e.target.files) {
handleFiles(e.target.files);
}
}
function formatSize(bytes: number) {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
return (
<div
data-slot="tron-file-upload"
className={cn("space-y-2", className)}
{...props}
>
<div
onClick={() => !disabled && inputRef.current?.click()}
onDragOver={(e) => {
e.preventDefault();
if (!disabled) {
setDragging(true);
}
}}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
className={cn(
"group relative flex cursor-pointer flex-col items-center justify-center rounded border-2 border-dashed px-6 py-10 transition-all",
disabled && "cursor-not-allowed opacity-40",
dragging
? "border-primary bg-primary/5 shadow-[0_0_20px_rgba(var(--primary-rgb,0,180,255),0.1)]"
: "border-primary/20 bg-card/60 hover:border-primary/40 hover:bg-card/80"
)}
>
{/* Upload icon */}
<svg
aria-hidden="true"
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
className={cn(
"mb-3 transition-colors",
dragging
? "text-primary"
: "text-foreground/20 group-hover:text-foreground/40"
)}
>
<path
d="M14 18V6M14 6l-5 5M14 6l5 5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 18v4a2 2 0 002 2h16a2 2 0 002-2v-4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
<span
className={cn(
"font-mono text-[10px] uppercase tracking-widest transition-colors",
dragging ? "text-primary" : "text-foreground/40"
)}
>
{label}
</span>
<span className="mt-1 font-mono text-[9px] text-foreground/20">
{description}
</span>
{maxSize ? (
<span className="mt-1 font-mono text-[8px] text-foreground/15">
MAX {formatSize(maxSize)}
</span>
) : null}
<input
ref={inputRef}
type="file"
accept={accept}
multiple={multiple}
onChange={handleChange}
className="hidden"
/>
{/* Corner decorations */}
<div className="pointer-events-none absolute top-1 left-1 h-3 w-3 border-primary/30 border-t border-l" />
<div className="pointer-events-none absolute top-1 right-1 h-3 w-3 border-primary/30 border-t border-r" />
<div className="pointer-events-none absolute bottom-1 left-1 h-3 w-3 border-primary/30 border-b border-l" />
<div className="pointer-events-none absolute right-1 bottom-1 h-3 w-3 border-primary/30 border-r border-b" />
</div>
{/* File list */}
{files.length > 0 && (
<div className="space-y-1">
{files.map((file, i) => (
<div
key={i}
className="flex items-center gap-2 rounded border border-primary/15 bg-card/60 px-3 py-1.5"
>
<svg
aria-hidden="true"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
className="shrink-0 text-primary/50"
>
<path
d="M2 1h5l3 3v7H2V1z"
stroke="currentColor"
strokeWidth="1"
/>
<path d="M7 1v3h3" stroke="currentColor" strokeWidth="1" />
</svg>
<span className="flex-1 truncate font-mono text-[10px] text-foreground/60">
{file.name}
</span>
<span className="font-mono text-[8px] text-foreground/25">
{formatSize(file.size)}
</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
const next = files.filter((_, j) => j !== i);
setFiles(next);
onFiles?.(next);
}}
className="flex h-4 w-4 items-center justify-center rounded text-foreground/25 hover:bg-red-500/10 hover:text-red-400"
>
<svg
aria-hidden="true"
width="6"
height="6"
viewBox="0 0 6 6"
fill="none"
>
<path
d="M0.5 0.5l5 5M5.5 0.5l-5 5"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
/>
</svg>
</button>
</div>
))}
</div>
)}
</div>
);
}
No selection tags yet — empty means no evidence, not lack of support.