# File Upload with Status Sections

> A file upload with status sections 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-06).

- Page, with a live preview: https://shadcn.exchange/r/blocks-so/file-upload-06
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/blocks-so/file-upload-06-light-7f17d6d00971eb4a.png
- Registry item: https://blocks.so/r/file-upload-06.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-06.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`, `progress`, `separator`

## Files

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

```tsx
'use client';

import { CheckCircle, FileText, Loader2, Upload, X } from 'lucide-react';
import type React from 'react';
import { useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Separator } from '@/components/ui/separator';

interface UploadItem {
  id: string;
  name: string;
  progress: number;
  status: 'uploading' | 'completed';
}

export default function FileUpload06() {
  const [uploads, setUploads] = useState<UploadItem[]>([
    {
      id: 'a1',
      name: 'design-mock-landing.png',
      progress: 62,
      status: 'uploading',
    },
    {
      id: 'b2',
      name: 'team-headshot-2025-01-09.jpg',
      progress: 28,
      status: 'uploading',
    },
    {
      id: 'c3',
      name: 'logo-v3-final.gif',
      progress: 100,
      status: 'completed',
    },
  ]);
  const filePickerRef = useRef<HTMLInputElement>(null);

  const openFilePicker = () => {
    filePickerRef.current?.click();
  };

  const onFileInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFiles = event.target.files;
    if (selectedFiles) {
      console.log('Files selected:', selectedFiles);
    }
  };

  const onDragOver = (event: React.DragEvent) => {
    event.preventDefault();
  };

  const onDropFiles = (event: React.DragEvent) => {
    event.preventDefault();
    const droppedFiles = event.dataTransfer.files;
    if (droppedFiles) {
      console.log('Files dropped:', droppedFiles);
    }
  };

  const removeUploadById = (id: string) => {
    setUploads(uploads.filter((file) => file.id !== id));
  };

  const activeUploads = uploads.filter((file) => file.status === 'uploading');
  const completedUploads = uploads.filter(
    (file) => file.status === 'completed'
  );

  return (
    <div className="mx-auto flex w-full max-w-sm flex-col gap-y-6">
      <Card
        className="group flex max-h-[200px] w-full cursor-pointer flex-col items-center justify-center gap-4 border-dashed py-8 text-sm shadow-none transition-colors hover:bg-muted/50"
        onClick={openFilePicker}
        onDragOver={onDragOver}
        onDrop={onDropFiles}
      >
        <div className="grid space-y-3">
          <div className="flex items-center gap-x-2 text-muted-foreground">
            <Upload className="size-5" />
            <div>
              Drop files here or{' '}
              <Button
                className="h-auto p-0 font-normal text-primary"
                onClick={openFilePicker}
                variant="link"
              >
                browse files
              </Button>{' '}
              to add
            </div>
          </div>
        </div>
        <input
          accept="image/png,image/jpeg,image/gif"
          className="hidden"
          multiple
          onChange={onFileInputChange}
          ref={filePickerRef}
          type="file"
        />
        <span className="mt-2 block text-base/6 text-muted-foreground group-disabled:opacity-50 sm:text-xs">
          Supported: JPG, PNG, GIF (max 10 MB)
        </span>
      </Card>

      <div className="flex flex-col gap-y-4">
        {activeUploads.length > 0 && (
          <div>
            <h2 className="mb-4 flex items-center text-balance font-mono font-normal text-foreground text-lg uppercase sm:text-xs">
              <Loader2 className="mr-1 size-4 animate-spin" />
              Uploading
            </h2>
            <div className="-mt-2 divide-y">
              {activeUploads.map((file) => (
                <div className="group flex items-center py-4" key={file.id}>
                  <div className="mr-3 grid size-10 shrink-0 place-content-center rounded border bg-muted">
                    <FileText className="inline size-4 group-hover:hidden" />
                    <Button
                      aria-label="Cancel"
                      className="hidden size-4 h-auto p-0 group-hover:inline"
                      onClick={() => removeUploadById(file.id)}
                      size="icon"
                      variant="ghost"
                    >
                      <X className="size-4" />
                    </Button>
                  </div>
                  <div className="mb-1 flex w-full flex-col">
                    <div className="flex justify-between gap-2">
                      <span className="select-none text-base/6 text-foreground group-disabled:opacity-50 sm:text-sm/6">
                        {file.name}
                      </span>
                      <span className="text-muted-foreground text-sm tabular-nums">
                        {file.progress}%
                      </span>
                    </div>
                    <Progress
                      className="mt-1 h-2 min-w-64"
                      value={file.progress}
                    />
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}

        {activeUploads.length > 0 && completedUploads.length > 0 && (
          <Separator className="my-0" />
        )}

        {completedUploads.length > 0 && (
          <div>
            <h2 className="mb-4 flex items-center text-balance font-mono font-normal text-foreground text-lg uppercase sm:text-xs">
              <CheckCircle className="mr-1 size-4" />
              Finished
            </h2>
            <div className="-mt-2 divide-y">
              {completedUploads.map((file) => (
                <div className="group flex items-center py-4" key={file.id}>
                  <div className="mr-3 grid size-10 shrink-0 place-content-center rounded border bg-muted">
                    <FileText className="inline size-4 group-hover:hidden" />
                    <Button
                      aria-label="Remove"
                      className="hidden size-4 h-auto p-0 group-hover:inline"
                      onClick={() => removeUploadById(file.id)}
                      size="icon"
                      variant="ghost"
                    >
                      <X className="size-4" />
                    </Button>
                  </div>
                  <div className="mb-1 flex w-full flex-col">
                    <div className="flex justify-between gap-2">
                      <span className="select-none text-base/6 text-foreground group-disabled:opacity-50 sm:text-sm/6">
                        {file.name}
                      </span>
                      <span className="text-muted-foreground text-sm tabular-nums">
                        {file.progress}%
                      </span>
                    </div>
                    <Progress
                      className="mt-1 h-2 min-w-64"
                      value={file.progress}
                    />
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
```
