- Overview
- Anatomy
- Header
- Navigation
- Renderers
Dropzone is the headless file-intake primitive. It owns drag/drop mechanics, file input wiring, accept validation, max-size and max-file rejection, controlled/uncontrolled file state, and accessibility prop getters.
Browser file intake is not upload. Dropzone decides which local files enter React state; your application decides whether, when, and how those files are uploaded.
It does not render icons, thumbnails, copy, grids, cards, progress rows, or upload transport.
Installation
pnpm dlx shadcn@latest add @retab/dropzone
Example
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
useDropzone,
type DropzoneFileItem,
} from "@/registry/new-york-v4/ui/dropzone";
import { formatFileSize } from "@/registry/new-york-v4/ui/file-size-format";Mental Model
A dropzone is not a rectangle. It is the browser file-intake behavior normalized into React state and prop getters.
filesis selected-file state.lastIntakeis the latest file-intake attempt.onFilesChangereports selected-file state changes.onIntakereports accepted and rejected files from one attempt.getTriggerPropsmakes any element open the file dialog; passnativewhen that element is a real<button>.
Trigger Patterns
getTriggerProps is the one opener. By default it polyfills button semantics
onto any element — role, focusability, and Enter/Space activation. Pass
native: true when the trigger is a real <button> so the platform owns those
and the getter only supplies the disabled attribute and type="button".
"use client";
import { Upload } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import {
InlineFileRows,
RejectionRows,"use client";
import { Paperclip } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import {
InlineFileRows,
RejectionRows,"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { useDropzone, type DropzoneFileItem } from "@/components/ui/dropzone";
import {
InlineFileRows,
RejectionRows,"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
useDropzone,
type DropzoneFileRejection,
} from "@/components/ui/dropzone";
"use client";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { type DropzoneExampleProps } from "./dropzone-example-shared";
export function DisabledDropzone({ className }: DropzoneExampleProps) {
const dropzone = useDropzone({ disabled: true, multiple: true });
File Compositions
Dropzone stays headless even when the visual surface is rich. These examples
compose it with FileThumbnail and FileViewer for selected file previews and
document intake flows.
"use client";
import { Upload } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {
RejectionRows,"use client";
import { cn } from "@/lib/utils";
import { type DropzoneExampleProps } from "./dropzone-example-shared";
import { FileIntakeViewer } from "./dropzone-uploader-viewer";
export function DropzoneFileViewerExample({ className }: DropzoneExampleProps) {
return <FileIntakeViewer className={cn("h-[34rem]", className)} />;
}"use client";
import { ImagePlus } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {
RejectionRows,"use client";
import { Table2, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { formatFileSize } from "@/components/ui/file-size-format";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {"use client";
import { FileAudio, Upload, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { formatFileSize } from "@/components/ui/file-size-format";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {Loading Bar
Intake is not upload. Once Dropzone admits a file into React state, your
application owns the transport. This example keys per-file upload progress off
the dropzone's file id and renders a progress bar; swap the simulated interval
for fetch or XMLHttpRequest with an onprogress handler in real code.
"use client";
import * as React from "react";
import { CheckCircle2, FileText, UploadCloud, X } from "lucide-react";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { useMountEffect } from "@/hooks/use-mount-effect";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { formatFileSize } from "@/components/ui/file-size-format";The file-intake viewer keeps browser acquisition separate from viewer chrome:
<FileIntakeViewerProvider>
<FileIntakeViewerDropTarget>
<FileIntakeViewerRoot>
<FileIntakeViewerHeader />
<ViewerBody>
<FileIntakeViewerSidebar />
<FileIntakeViewerSurface />
</ViewerBody>
</FileIntakeViewerRoot>
</FileIntakeViewerDropTarget>
</FileIntakeViewerProvider>FileIntakeViewerDropTarget owns the drop root and hidden file input.
FileIntakeViewerRoot owns only the viewer frame.
Workflow Surfaces
Custom products usually need file intake inside a domain-specific state shape: comparison slots, required packet checklists, routed intake lanes, timelines, or pinboards. Dropzone owns intake; the surrounding component owns the workflow.
"use client";
import { Clock3, Paperclip, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { formatFileSize } from "@/components/ui/file-size-format";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {"use client";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { formatFileSize } from "@/components/ui/file-size-format";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {"use client";
import { Upload, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useDropzone, type DropzoneFileItem } from "@/components/ui/dropzone";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {
RejectionRows,"use client";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {
RejectionRows,
type DropzoneExampleProps,
} from "./dropzone-example-shared";"use client";
import { cn } from "@/lib/utils";
import { useDropzone } from "@/components/ui/dropzone";
import { FileThumbnail } from "@/components/ui/file-thumbnail";
import {
RejectionRows,
type DropzoneExampleProps,
} from "./dropzone-example-shared";Behavior
- Uses the same commit path for drag/drop and native file input selection.
- Opens the native file picker through
openFileDialogor trigger props. - Tracks nested drag enter/leave events so active state does not flicker.
- Highlights only when the drag payload contains files.
- Accepts MIME types, wildcard MIME types such as
image/*, and extensions. - Supports controlled and uncontrolled file state.
- Supports
multiple,maxFiles, andmaxSize. - Exposes
removeFile,clearFiles,resetIntake, andreset. - Reports each intake attempt through
lastIntakeandonIntake. - Reports rejection facts, not display copy.
- Composes consumer event handlers and respects
event.defaultPrevented.
API Reference
useDropzone
| Prop | Type | Default | Required |
|---|---|---|---|
accept | string | - | No |
defaultFiles | DropzoneFileItem[] | [] | No |
disabled | boolean | false | No |
files | DropzoneFileItem[] | - | No |
maxFiles | number | - | No |
maxSize | number | - | No |
multiple | boolean | true | No |
onFilesChange | (files: DropzoneFileItem[]) => void | - | No |
onIntake | (intake: DropzoneIntake) => void | - | No |
Return Value
| Field | Type |
|---|---|
files | DropzoneFileItem[] |
lastIntake | DropzoneIntake |
isDragging | boolean |
isDisabled | boolean |
clearFiles | () => void |
resetIntake | () => void |
reset | () => void |
openFileDialog | () => void |
removeFile | (fileId: string) => void |
getRootProps | (props?) => React.HTMLAttributes<HTMLElement> |
getInputProps | (props?) => React.InputHTMLAttributes<HTMLInputElement> |
getTriggerProps | (props? & { native?: boolean }) => props |
The package also exports the public types DropzoneFileItem,
DropzoneIntake, DropzoneFileRejection, UseDropzoneProps, and
UseDropzoneReturn.
Validation helpers are exported for tests and advanced compositions:
parseDropzoneAccept, matchesDropzoneAccept, validateDropzoneFile, and
validateDropzoneFiles.
DropzoneFileRejection is a discriminated union:
| Reason | Extra fields |
|---|---|
file-invalid-type | acceptRules |
file-too-large | maxSize |
too-many-files | maxFiles |
The primitive does not export rejection messages or file-size formatting. Render copy in the visual layer.
Source
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import {
formatDropzoneAccept,
parseDropzoneAccept,
validateDropzoneFiles,
type DropzoneAcceptRule,
type DropzoneFileRejection,