Click to upload or drag and drop
"use client"
import * as React from "react"
import { FileDropzone } from "@/components/ui/file-dropzone"
export default function FileDropzoneDemo() {
const [files, setFiles] = React.useState<string[]>([])
return (
<div className="w-full max-w-sm space-y-3">
<FileDropzone
multiple
onFiles={(f) => setFiles(f.map((file) => file.name))}
/>
{files.length > 0 && (
<ul className="text-muted-foreground space-y-1 text-sm">
{files.map((name) => (
<li key={name} className="truncate">
{name}
</li>
))}
</ul>
)}
</div>
)
}Installation
npx logic2b@latest add file-dropzoneWorking with an AI assistant? Copy this prompt into Claude Code, Cursor or Copilot — it carries the install steps, a no-CLI fallback and a verification checklist.
Add the "file-dropzone" item from the logic2b ui registry (https://ui.logic2b.com) to this project. Follow every step and verify at the end. ## Steps 1. If the project has no `components.json` yet, set up logic2b ui first: ```bash npx logic2b@latest init ``` 2. Install the item (the CLI resolves registry dependencies automatically): ```bash npx logic2b@latest add file-dropzone ``` 3. Install any npm dependencies the command prints. ## If the CLI is not available Do it manually from the registry payload: 1. Fetch https://ui.logic2b.com/r/file-dropzone.json. 2. Write every entry in `files[]` to its mapped project path (see below). 3. Repeat (recursively) for each name in `registryDependencies`. 4. Install every package listed in `dependencies` across those payloads. ## Registry reference - Index of every item: https://ui.logic2b.com/r/index.json - Item payload (full source): https://ui.logic2b.com/r/<name>.json - Usage examples per item: https://ui.logic2b.com/r/demos/index.json → https://ui.logic2b.com/r/demos/<demo>.json - Docs index for agents: https://ui.logic2b.com/llms.txt (full docs: https://ui.logic2b.com/llms-full.txt) - Every docs page is Markdown when you append `.md` to its URL. Registry file paths map to project paths like this: - `ui/*` → `@/components/ui/*` - `blocks/<name>/*` → `@/components/*` - `charts/*` → `@/components/charts/*` - `hooks/*` → `@/hooks/*` - `lib/utils.ts` → `@/lib/utils.ts` - `theme.css` → next to the project's Tailwind entry stylesheet ## Conventions (do not break these) - Tailwind CSS v4 (CSS-first config; no tailwind.config.js needed) and React 19. - All colors go through semantic tokens (`bg-primary`, `text-muted-foreground`, `border-border`, …). Never hardcode hex/oklch values in components. - Dark mode is class-based: toggle `.dark` on `<html>`. - Icons come from lucide-react. ## Verify before finishing 1. The dev server starts and the app renders without console errors. 2. TypeScript compiles (`tsc --noEmit` or the framework's check). 3. Components use the theme: a `<Button>` shows the primary token color, and toggling `.dark` on `<html>` switches the palette.
Install the dependencies:
npm install lucide-reactCopy into ui/file-dropzone.tsx:
"use client"
import * as React from "react"
import { UploadIcon } from "lucide-react"
import { cn } from "@/lib/utils"
export interface FileDropzoneProps
extends Omit<React.ComponentProps<"div">, "onDrop" | "children"> {
/** Called with the accepted files when they are dropped or selected. */
onFiles?: (files: File[]) => void
/** `accept` attribute forwarded to the file input (e.g. `"image/*"`). */
accept?: string
/** Allow selecting more than one file. Default `false`. */
multiple?: boolean
disabled?: boolean
/** Replace the default icon/prompt with your own content. */
children?: React.ReactNode
}
/**
* A drag-and-drop file surface with click / keyboard to browse. Native DnD (no
* dependencies): highlights while a file is dragged over it and calls `onFiles`
* with the dropped or selected files. Rendering the file list is left to the
* consumer.
*/
function FileDropzone({
className,
onFiles,
accept,
multiple = false,
disabled,
children,
...props
}: FileDropzoneProps) {
const inputRef = React.useRef<HTMLInputElement>(null)
const [dragging, setDragging] = React.useState(false)
const emit = (list: FileList | null) => {
if (!list || list.length === 0) return
onFiles?.(Array.from(list))
}
const open = () => {
if (!disabled) inputRef.current?.click()
}
return (
<div
data-slot="file-dropzone"
data-dragging={dragging ? "" : undefined}
data-disabled={disabled ? "" : undefined}
role="button"
tabIndex={disabled ? -1 : 0}
aria-disabled={disabled}
onClick={open}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
open()
}
}}
onDragOver={(e) => {
e.preventDefault()
if (!disabled) setDragging(true)
}}
onDragLeave={() => setDragging(false)}
onDrop={(e) => {
e.preventDefault()
setDragging(false)
if (!disabled) emit(e.dataTransfer.files)
}}
className={cn(
"border-input flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed p-8 text-center transition-colors outline-none",
"hover:bg-accent/40 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"data-dragging:border-ring data-dragging:bg-accent/60",
"data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
{...props}
>
<input
ref={inputRef}
type="file"
accept={accept}
multiple={multiple}
disabled={disabled}
className="sr-only"
onChange={(e) => {
emit(e.target.files)
e.target.value = ""
}}
/>
{children ?? (
<>
<UploadIcon className="text-muted-foreground size-6" />
<div className="text-sm">
<span className="font-medium">Click to upload</span>
<span className="text-muted-foreground"> or drag and drop</span>
</div>
</>
)}
</div>
)
}
export { FileDropzone }Usage
import { FileDropzone } from "@/components/ui/file-dropzone"
<FileDropzone
multiple
accept="image/*"
onFiles={(files) => upload(files)}
/>
It highlights while a file is dragged over it and calls onFiles with the
dropped or selected File[]. Rendering the file list, previews and upload
progress is left to you — pass your own content as children to replace the
default icon and prompt.
Accessibility
The surface is a role="button" region, focusable and operable with
Enter / Space to open the file picker, so it works without
a mouse. It wraps a real <input type="file">, so native file selection and
accept/multiple behave as expected.
API
| Prop | Type | Default |
|---|---|---|
onFiles |
(files: File[]) => void |
— |
accept |
string |
— |
multiple |
boolean |
false |
disabled |
boolean |
false |
children |
React.ReactNode |
default icon + prompt |
| …props | React.ComponentProps<"div"> |
— |