# File Upload Drag & Drop

> A file upload drag & drop block.

A shadcn/ui block from the [@blocks-so](https://blocks.so) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/blocks-so/file-upload-04).

- Page, with a live preview: https://shadcn.exchange/r/blocks-so/file-upload-04
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/blocks-so/file-upload-04-light-fe833630e4ec6127.png
- Registry item: https://blocks.so/r/file-upload-04.json
- Categories: file-upload
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://blocks.so/r/file-upload-04.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `sonner`
- Registry items: `button`, `card`, `progress`

## Files

### `content/components/file-upload/file-upload-04.tsx`

```tsx
'use client';

import { File, FileSpreadsheet, X } from 'lucide-react';
import { type ChangeEvent, type DragEvent, useRef, useState } from 'react';
import { toast } from 'sonner';

import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';

export default function FileUpload04() {
  const [uploadState, setUploadState] = useState<{
    file: File | null;
    progress: number;
    uploading: boolean;
  }>({
    file: null,
    progress: 0,
    uploading: false,
  });
  const [showDummy, setShowDummy] = useState(true);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const validFileTypes = [
    'text/csv',
    'application/vnd.ms-excel',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  ];

  const handleFile = (file: File | undefined) => {
    if (!file) return;

    if (validFileTypes.includes(file.type)) {
      setUploadState({ file, progress: 0, uploading: true });

      const interval = setInterval(() => {
        setUploadState((prev) => {
          const newProgress = prev.progress + 5;
          if (newProgress >= 100) {
            clearInterval(interval);
            return { ...prev, progress: 100, uploading: false };
          }
          return { ...prev, progress: newProgress };
        });
      }, 200);
    } else {
      toast.error('Please upload a CSV, XLSX, or XLS file.', {
        position: 'bottom-right',
        duration: 3000,
      });
    }
  };

  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
    handleFile(event.target.files?.[0]);
  };

  const handleDrop = (event: DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    handleFile(event.dataTransfer.files?.[0]);
  };

  const resetFile = () => {
    setUploadState({ file: null, progress: 0, uploading: false });
    if (fileInputRef.current) {
      fileInputRef.current.value = '';
    }
  };

  const getFileIcon = () => {
    if (!uploadState.file) return <File />;

    const fileExt = uploadState.file.name.split('.').pop()?.toLowerCase() || '';
    return ['csv', 'xlsx', 'xls'].includes(fileExt) ? (
      <FileSpreadsheet className="h-5 w-5 text-foreground" />
    ) : (
      <File className="h-5 w-5 text-foreground" />
    );
  };

  const formatFileSize = (bytes: number) => {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return Number.parseFloat((bytes / k ** i).toFixed(1)) + ' ' + sizes[i];
  };

  const { file, progress, uploading } = uploadState;

  return (
    <div className="flex w-full max-w-lg items-center justify-center p-10">
      <form className="w-full" onSubmit={(e) => e.preventDefault()}>
        <h3 className="text-balance font-semibold text-foreground text-lg">
          File Upload
        </h3>

        <div
          className="mt-2 flex justify-center rounded-md border border-input border-dashed px-6 py-12"
          onDragOver={(e) => e.preventDefault()}
          onDrop={handleDrop}
        >
          <div>
            <File
              aria-hidden={true}
              className="mx-auto h-12 w-12 text-muted-foreground"
            />
            <div className="flex text-muted-foreground text-sm leading-6">
              <p>Drag and drop or</p>
              <label
                className="relative cursor-pointer rounded-sm pl-1 font-medium text-primary hover:underline hover:underline-offset-4"
                htmlFor="file-upload-03"
              >
                <span>choose file</span>
                <input
                  accept=".csv, .xlsx, .xls"
                  className="sr-only"
                  id="file-upload-03"
                  name="file-upload-03"
                  onChange={handleFileChange}
                  ref={fileInputRef}
                  type="file"
                />
              </label>
              <p className="text-pretty pl-1">to upload</p>
            </div>
          </div>
        </div>

        <p className="mt-2 text-pretty text-muted-foreground text-xs leading-5 sm:flex sm:items-center sm:justify-between">
          <span>Accepted file types: CSV, XLSX or XLS files.</span>
          <span className="pl-1 sm:pl-0">Max. size: 10MB</span>
        </p>

        {!file && showDummy && (
          <Card className="relative mt-8 gap-4 bg-muted p-4 shadow-none">
            <Button
              aria-label="Remove"
              className="absolute top-1 right-1 text-muted-foreground hover:text-foreground"
              onClick={() => setShowDummy(false)}
              size="icon-sm"
              type="button"
              variant="ghost"
            >
              <X aria-hidden={true} className="h-5 w-5 shrink-0" />
            </Button>

            <div className="flex items-center space-x-2.5">
              <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-sm bg-background shadow-sm ring-1 ring-border ring-inset">
                <FileSpreadsheet
                  aria-hidden={true}
                  className="h-5 w-5 text-foreground"
                />
              </span>
              <div>
                <p className="text-pretty font-medium text-foreground text-xs">
                  Revenue_Q1_2024.xlsx
                </p>
                <p className="mt-0.5 text-pretty text-muted-foreground text-xs">
                  3.1 MB
                </p>
              </div>
            </div>

            <div className="flex items-center space-x-3">
              <Progress className="h-1.5" value={45} />
              <span className="text-muted-foreground text-xs">45%</span>
            </div>
          </Card>
        )}

        {file && (
          <Card className="relative mt-8 gap-4 bg-muted p-4 shadow-none">
            <Button
              aria-label="Remove"
              className="absolute top-1 right-1 text-muted-foreground hover:text-foreground"
              onClick={resetFile}
              size="icon-sm"
              type="button"
              variant="ghost"
            >
              <X aria-hidden={true} className="h-5 w-5 shrink-0" />
            </Button>

            <div className="flex items-center space-x-2.5">
              <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-sm bg-background shadow-sm ring-1 ring-border ring-inset">
                {getFileIcon()}
              </span>
              <div>
                <p className="text-pretty font-medium text-foreground text-xs">
                  {file?.name}
                </p>
                <p className="mt-0.5 text-pretty text-muted-foreground text-xs">
                  {file && formatFileSize(file.size)}
                </p>
              </div>
            </div>

            <div className="flex items-center space-x-3">
              <Progress className="h-1.5" value={progress} />
              <span className="text-muted-foreground text-xs">{progress}%</span>
            </div>
          </Card>
        )}

        <div className="mt-8 flex items-center justify-end space-x-3">
          <Button
            className="whitespace-nowrap"
            disabled={!file}
            onClick={resetFile}
            type="button"
            variant="outline"
          >
            Cancel
          </Button>
          <Button
            className="whitespace-nowrap"
            disabled={!file || uploading || progress < 100}
            type="submit"
          >
            Upload
          </Button>
        </div>
      </form>
    </div>
  );
}
```
