# File Upload with Preview

> A file upload with preview 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-01).

- Page, with a live preview: https://shadcn.exchange/r/blocks-so/file-upload-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/blocks-so/file-upload-01-light-e67ec481730a90e2.png
- Registry item: https://blocks.so/r/file-upload-01.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-01.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`
- Registry items: `button`, `card`, `input`, `label`, `select`, `tooltip`

## Files

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

```tsx
'use client';

import { HelpCircle, Trash2, Upload } from 'lucide-react';
import { useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';

export default function FileUpload01() {
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
  const [fileProgresses, setFileProgresses] = useState<Record<string, number>>(
    {}
  );

  const handleFileSelect = (files: FileList | null) => {
    if (!files) return;

    const newFiles = Array.from(files);
    setUploadedFiles((prev) => [...prev, ...newFiles]);

    // Simulate upload progress for each file
    newFiles.forEach((file) => {
      let progress = 0;
      const interval = setInterval(() => {
        progress += Math.random() * 10;
        if (progress >= 100) {
          progress = 100;
          clearInterval(interval);
        }
        setFileProgresses((prev) => ({
          ...prev,
          [file.name]: Math.min(progress, 100),
        }));
      }, 300);
    });
  };

  const handleBoxClick = () => {
    fileInputRef.current?.click();
  };

  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault();
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    handleFileSelect(e.dataTransfer.files);
  };

  const removeFile = (filename: string) => {
    setUploadedFiles((prev) => prev.filter((file) => file.name !== filename));
    setFileProgresses((prev) => {
      const newProgresses = { ...prev };
      delete newProgresses[filename];
      return newProgresses;
    });
  };

  return (
    <div className="flex items-center justify-center p-10">
      <Card className="mx-auto w-full max-w-lg rounded-lg bg-background p-0 shadow-md">
        <CardContent className="p-0">
          <div className="p-6 pb-4">
            <div className="flex items-start justify-between">
              <div>
                <h2 className="text-balance font-medium text-foreground text-lg">
                  Create a new project
                </h2>
                <p className="mt-1 text-pretty text-muted-foreground text-sm">
                  Drag and drop files to create a new project.
                </p>
              </div>
            </div>
          </div>

          <div className="mt-2 px-6 pb-4">
            <div className="grid grid-cols-2 gap-4">
              <div>
                <Label className="mb-2" htmlFor="projectName">
                  Project name
                </Label>
                <Input
                  defaultValue="Open Source Stripe"
                  id="projectName"
                  type="text"
                />
              </div>

              <div>
                <Label className="mb-2" htmlFor="projectLead">
                  Project lead
                </Label>
                <Select
                  defaultValue="1"
                  items={{
                    '1': 'Ephraim Duncan',
                    '2': 'Lucas Smith',
                    '3': 'Timur Ercan',
                  }}
                >
                  <SelectTrigger className="w-full ps-2" id="projectLead">
                    <SelectValue placeholder="Select project lead" />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectGroup>
                      <SelectItem value="1">
                        <img
                          alt="Ephraim Duncan"
                          className="size-5 rounded"
                          height={20}
                          src="https://blocks.so/avatar-01.png"
                          width={20}
                        />
                        <span className="truncate">Ephraim Duncan</span>
                      </SelectItem>
                      <SelectItem value="2">
                        <img
                          alt="Lucas Smith"
                          className="size-5 rounded"
                          height={20}
                          src="https://blocks.so/avatar-03.png"
                          width={20}
                        />
                        <span className="truncate">Lucas Smith</span>
                      </SelectItem>
                      <SelectItem value="3">
                        <img
                          alt="Timur Ercan"
                          className="size-5 rounded"
                          height={20}
                          src="https://blocks.so/avatar-02.jpg"
                          width={20}
                        />
                        <span className="truncate">Timur Ercan</span>
                      </SelectItem>
                    </SelectGroup>
                  </SelectContent>
                </Select>
              </div>
            </div>
          </div>

          <div className="px-6">
            <div
              className="flex cursor-pointer flex-col items-center justify-center rounded-md border-2 border-border border-dashed p-8 text-center"
              onClick={handleBoxClick}
              onDragOver={handleDragOver}
              onDrop={handleDrop}
            >
              <div className="mb-2 rounded-full bg-muted p-3">
                <Upload className="h-5 w-5 text-muted-foreground" />
              </div>
              <p className="text-pretty font-medium text-foreground text-sm">
                Upload a project image
              </p>
              <p className="mt-1 text-pretty text-muted-foreground text-sm">
                or,{' '}
                <label
                  className="cursor-pointer font-medium text-primary hover:text-primary/90"
                  htmlFor="fileUpload"
                  onClick={(e) => e.stopPropagation()}
                >
                  click to browse
                </label>{' '}
                (4MB max)
              </p>
              <input
                accept="image/*"
                className="hidden"
                id="fileUpload"
                onChange={(e) => handleFileSelect(e.target.files)}
                ref={fileInputRef}
                type="file"
              />
            </div>
          </div>

          <div
            className={cn(
              'space-y-3 px-6 pb-5',
              uploadedFiles.length > 0 ? 'mt-4' : ''
            )}
          >
            {uploadedFiles.map((file, index) => {
              const imageUrl = URL.createObjectURL(file);

              return (
                <div
                  className="flex flex-col rounded-lg border border-border p-2"
                  key={file.name + index}
                  onLoad={() => {
                    return () => URL.revokeObjectURL(imageUrl);
                  }}
                >
                  <div className="flex items-center gap-2">
                    <div className="row-span-2 flex h-14 w-18 items-center justify-center self-start overflow-hidden rounded-sm bg-muted">
                      <img
                        alt={file.name}
                        className="h-full w-full object-cover"
                        src={imageUrl}
                      />
                    </div>

                    <div className="flex-1 pr-1">
                      <div className="flex items-center justify-between">
                        <div className="flex items-center gap-2">
                          <span className="max-w-[250px] truncate text-foreground text-sm">
                            {file.name}
                          </span>
                          <span className="whitespace-nowrap text-muted-foreground text-sm">
                            {Math.round(file.size / 1024)} KB
                          </span>
                        </div>
                        <Button
                          className="bg-transparent! hover:text-red-500"
                          onClick={() => removeFile(file.name)}
                          size="icon-sm"
                          variant="ghost"
                        >
                          <Trash2 className="h-4 w-4" />
                        </Button>
                      </div>

                      <div className="flex items-center gap-2">
                        <div className="h-2 flex-1 overflow-hidden rounded-full bg-muted">
                          <div
                            className="h-full bg-primary"
                            style={{
                              width: `${fileProgresses[file.name] || 0}%`,
                            }}
                          />
                        </div>
                        <span className="whitespace-nowrap text-muted-foreground text-xs">
                          {Math.round(fileProgresses[file.name] || 0)}%
                        </span>
                      </div>
                    </div>
                  </div>
                </div>
              );
            })}
          </div>

          <div className="flex items-center justify-between rounded-b-lg border-border border-t bg-muted px-6 py-3">
            <TooltipProvider delay={0}>
              <Tooltip>
                <TooltipTrigger
                  render={
                    <Button
                      className="flex items-center text-muted-foreground hover:text-foreground"
                      size="sm"
                      variant="ghost"
                    />
                  }
                >
                  <HelpCircle className="mr-1 h-4 w-4" />
                  Need help?
                </TooltipTrigger>
                <TooltipContent className="border bg-background py-3 text-foreground">
                  <div className="space-y-1">
                    <p className="text-pretty font-medium text-[13px]">
                      Need assistance?
                    </p>
                    <p className="max-w-[200px] text-pretty text-muted-foreground text-xs dark:text-muted-background">
                      Upload project images by dragging and dropping files or
                      using the file browser. Supported formats: JPG, PNG, SVG.
                      Maximum file size: 4MB.
                    </p>
                  </div>
                </TooltipContent>
              </Tooltip>
            </TooltipProvider>

            <div className="flex gap-2">
              <Button
                className="h-9 px-4 font-medium text-sm"
                variant="outline"
              >
                Cancel
              </Button>
              <Button className="h-9 px-4 font-medium text-sm">Continue</Button>
            </div>
          </div>
        </CardContent>
      </Card>
    </div>
  );
}
```
