From d80690bf5d7996d9b8d8eb2dce54bf44440b4cfb Mon Sep 17 00:00:00 2001 From: JAYATIAHUJA Date: Sun, 9 Aug 2026 02:49:11 +0530 Subject: [PATCH 1/3] feat(upload-state): add accessible file drop zone --- apps/www/registry/magicui/upload-state.tsx | 232 +++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 apps/www/registry/magicui/upload-state.tsx diff --git a/apps/www/registry/magicui/upload-state.tsx b/apps/www/registry/magicui/upload-state.tsx new file mode 100644 index 000000000..5438eb696 --- /dev/null +++ b/apps/www/registry/magicui/upload-state.tsx @@ -0,0 +1,232 @@ +"use client" + +import { forwardRef, useId, useRef, useState } from "react" +import type { ComponentPropsWithoutRef, DragEvent, KeyboardEvent } from "react" +import { + CloudUploadIcon, + FileCheck2Icon, + LoaderCircleIcon, + TriangleAlertIcon, +} from "lucide-react" + +import { cn } from "@/lib/utils" + +export type UploadStateStatus = "idle" | "uploading" | "success" | "error" + +export interface UploadStateProps extends ComponentPropsWithoutRef<"div"> { + accept?: string + multiple?: boolean + disabled?: boolean + status?: UploadStateStatus + progress?: number + error?: string + onFiles?: (files: File[]) => void +} + +export const UploadState = forwardRef( + ( + { + accept, + multiple = false, + disabled = false, + status = "idle", + progress, + error, + onFiles, + className, + onClick, + onDragEnter, + onDragLeave, + onDragOver, + onDrop, + onKeyDown, + "aria-describedby": ariaDescribedBy, + ...props + }, + ref + ) => { + const inputRef = useRef(null) + const [isDragging, setIsDragging] = useState(false) + const descriptionId = useId() + const isInteractive = !disabled && status !== "uploading" + const hasProgress = + typeof progress === "number" && Number.isFinite(progress) + const normalizedProgress = hasProgress + ? Math.min(100, Math.max(0, progress)) + : undefined + + const selectFiles = (files: FileList | null) => { + const selectedFiles = Array.from(files ?? []) + + if (selectedFiles.length > 0) { + onFiles?.(selectedFiles) + } + } + + const openFilePicker = () => { + if (isInteractive) { + inputRef.current?.click() + } + } + + const handleKeyDown = (event: KeyboardEvent) => { + onKeyDown?.(event) + + if (event.defaultPrevented || !isInteractive) return + + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + openFilePicker() + } + } + + const handleDragEnter = (event: DragEvent) => { + onDragEnter?.(event) + + event.preventDefault() + if (!isInteractive) return + + setIsDragging(true) + } + + const handleDragOver = (event: DragEvent) => { + onDragOver?.(event) + + event.preventDefault() + if (!isInteractive) return + + event.dataTransfer.dropEffect = "copy" + setIsDragging(true) + } + + const handleDragLeave = (event: DragEvent) => { + onDragLeave?.(event) + + if (event.currentTarget.contains(event.relatedTarget as Node)) return + + setIsDragging(false) + } + + const handleDrop = (event: DragEvent) => { + onDrop?.(event) + + event.preventDefault() + setIsDragging(false) + + if (!isInteractive) return + + selectFiles(event.dataTransfer.files) + } + + const message = isDragging + ? "Drop files to add them" + : status === "uploading" + ? normalizedProgress === undefined + ? "Uploading files" + : `Uploading files, ${normalizedProgress}% complete` + : status === "success" + ? multiple + ? "Files are ready" + : "File is ready" + : status === "error" + ? (error ?? "Something went wrong. Try another file.") + : "Drop files here or click to browse" + + return ( +
{ + onClick?.(event) + + if (!event.defaultPrevented) { + openFilePicker() + } + }} + onKeyDown={handleKeyDown} + onDragEnter={handleDragEnter} + onDragLeave={handleDragLeave} + onDragOver={handleDragOver} + onDrop={handleDrop} + {...props} + > + event.stopPropagation()} + onChange={(event) => { + selectFiles(event.target.files) + event.target.value = "" + }} + /> + + + +

{message}

+ {status === "idle" && !isDragging && ( +

+ {multiple ? "Choose one or more files" : "Choose a file"} +

+ )} + {status === "uploading" && normalizedProgress !== undefined && ( +
+
+
+ )} + + {message} + +
+ ) + } +) + +UploadState.displayName = "UploadState" From eabd937887e61f5c8055ae9439881c2d0373b694 Mon Sep 17 00:00:00 2001 From: JAYATIAHUJA Date: Sun, 9 Aug 2026 02:50:34 +0530 Subject: [PATCH 2/3] docs(upload-state): add interactive demo --- .../registry/example/upload-state-demo.tsx | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 apps/www/registry/example/upload-state-demo.tsx diff --git a/apps/www/registry/example/upload-state-demo.tsx b/apps/www/registry/example/upload-state-demo.tsx new file mode 100644 index 000000000..eafc6de98 --- /dev/null +++ b/apps/www/registry/example/upload-state-demo.tsx @@ -0,0 +1,92 @@ +"use client" + +import { useEffect, useRef, useState } from "react" + +import { Button } from "@/components/ui/button" +import { + UploadState, + type UploadStateStatus, +} from "@/registry/magicui/upload-state" + +export default function UploadStateDemo() { + const [status, setStatus] = useState("idle") + const [progress, setProgress] = useState(0) + const [fileName, setFileName] = useState() + const uploadTimer = useRef>(undefined) + + const clearUploadTimer = () => { + if (uploadTimer.current) { + clearInterval(uploadTimer.current) + uploadTimer.current = undefined + } + } + + useEffect(() => { + return () => { + if (uploadTimer.current) { + clearInterval(uploadTimer.current) + } + } + }, []) + + const handleFiles = (files: File[]) => { + clearUploadTimer() + setFileName( + files.length === 1 ? files[0].name : `${files.length} files selected` + ) + setProgress(0) + setStatus("uploading") + + let nextProgress = 0 + uploadTimer.current = setInterval(() => { + nextProgress += 10 + setProgress(nextProgress) + + if (nextProgress >= 100) { + clearUploadTimer() + setStatus("success") + } + }, 220) + } + + const reset = () => { + clearUploadTimer() + setStatus("idle") + setProgress(0) + setFileName(undefined) + } + + return ( +
+ +
+

+ {fileName ?? "Choose an image or PDF to start"} +

+
+ + +
+
+
+ ) +} From 09a969a56e28c262a41ac73f916d92b8face4c96 Mon Sep 17 00:00:00 2001 From: JAYATIAHUJA Date: Sun, 9 Aug 2026 02:55:05 +0530 Subject: [PATCH 3/3] feat(upload-state): register component and docs --- apps/www/config/docs.ts | 6 + .../content/docs/components/upload-state.mdx | 84 +++++ apps/www/public/llms-full.txt | 338 ++++++++++++++++++ apps/www/public/llms.txt | 2 + apps/www/public/r/registry.json | 30 ++ apps/www/public/r/upload-state-demo.json | 17 + apps/www/public/r/upload-state.json | 17 + apps/www/public/registry.json | 30 ++ apps/www/registry.json | 30 ++ apps/www/registry/__index__.tsx | 34 ++ apps/www/registry/registry-examples.ts | 14 + apps/www/registry/registry-ui.ts | 14 + 12 files changed, 616 insertions(+) create mode 100644 apps/www/content/docs/components/upload-state.mdx create mode 100644 apps/www/public/r/upload-state-demo.json create mode 100644 apps/www/public/r/upload-state.json diff --git a/apps/www/config/docs.ts b/apps/www/config/docs.ts index ddb7c84f8..b0b3ce5f2 100644 --- a/apps/www/config/docs.ts +++ b/apps/www/config/docs.ts @@ -579,6 +579,12 @@ export const docsConfig: DocsConfig = { items: [], label: "", }, + { + title: "Upload State", + href: `/docs/components/upload-state`, + items: [], + label: "", + }, { title: "Code Comparison", href: `/docs/components/code-comparison`, diff --git a/apps/www/content/docs/components/upload-state.mdx b/apps/www/content/docs/components/upload-state.mdx new file mode 100644 index 000000000..63fbaf2dc --- /dev/null +++ b/apps/www/content/docs/components/upload-state.mdx @@ -0,0 +1,84 @@ +--- +title: Upload State +date: 2026-08-09 +description: An accessible file drop area with controlled upload states and progress. +author: JAYATIAHUJA +published: true +--- + + + +## Installation + + + + + CLI + Manual + + + + +```bash +npx shadcn@latest add @magicui/upload-state +``` + + + + + + + +Copy and paste the following code into your project. + + + +Update the import paths to match your project setup. + + + + + + + +## Usage + +```tsx showLineNumbers +import { UploadState } from "@/components/ui/upload-state" +``` + +```tsx showLineNumbers + { + console.log(files) + }} +/> +``` + +`UploadState` handles file selection and dropping. Your app decides what happens next, including validation, upload requests, progress updates, and errors. + +## Props + +| Prop | Type | Default | Description | +| ---------- | ----------------------------------------------- | -------- | ------------------------------------------------------------------- | +| `accept` | `string` | `-` | File types accepted by the native file picker. | +| `multiple` | `boolean` | `false` | Whether users can select more than one file. | +| `disabled` | `boolean` | `false` | Prevents file selection and dropping. | +| `status` | `"idle" \| "uploading" \| "success" \| "error"` | `"idle"` | The state displayed by the component. | +| `progress` | `number` | `-` | Upload progress from `0` to `100`. Values outside this range clamp. | +| `error` | `string` | `-` | The message displayed when `status` is `"error"`. | +| `onFiles` | `(files: File[]) => void` | `-` | Called after files are chosen with the picker or dropped. | + +The component also accepts normal `div` props such as `className`, `id`, and `aria-*` attributes. + +## Accessibility + +The drop area supports click, Enter, Space, and native file selection. It announces state changes and exposes progress to assistive technology. File selection is locked while the component is uploading or disabled. + +## Credits + +- Built by [@JAYATIAHUJA](https://github.com/JAYATIAHUJA) diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt index d8bda350f..937fb5e8b 100644 --- a/apps/www/public/llms-full.txt +++ b/apps/www/public/llms-full.txt @@ -20177,6 +20177,344 @@ export default function Component() { +===== COMPONENT: upload-state ===== +Title: Upload State +Description: An accessible file drop area with controlled upload states and progress. + +--- file: magicui/upload-state.tsx --- +"use client" + +import { forwardRef, useId, useRef, useState } from "react" +import type { ComponentPropsWithoutRef, DragEvent, KeyboardEvent } from "react" +import { + CloudUploadIcon, + FileCheck2Icon, + LoaderCircleIcon, + TriangleAlertIcon, +} from "lucide-react" + +import { cn } from "@/lib/utils" + +export type UploadStateStatus = "idle" | "uploading" | "success" | "error" + +export interface UploadStateProps extends ComponentPropsWithoutRef<"div"> { + accept?: string + multiple?: boolean + disabled?: boolean + status?: UploadStateStatus + progress?: number + error?: string + onFiles?: (files: File[]) => void +} + +export const UploadState = forwardRef( + ( + { + accept, + multiple = false, + disabled = false, + status = "idle", + progress, + error, + onFiles, + className, + onClick, + onDragEnter, + onDragLeave, + onDragOver, + onDrop, + onKeyDown, + "aria-describedby": ariaDescribedBy, + ...props + }, + ref + ) => { + const inputRef = useRef(null) + const [isDragging, setIsDragging] = useState(false) + const descriptionId = useId() + const isInteractive = !disabled && status !== "uploading" + const hasProgress = + typeof progress === "number" && Number.isFinite(progress) + const normalizedProgress = hasProgress + ? Math.min(100, Math.max(0, progress)) + : undefined + + const selectFiles = (files: FileList | null) => { + const selectedFiles = Array.from(files ?? []) + + if (selectedFiles.length > 0) { + onFiles?.(selectedFiles) + } + } + + const openFilePicker = () => { + if (isInteractive) { + inputRef.current?.click() + } + } + + const handleKeyDown = (event: KeyboardEvent) => { + onKeyDown?.(event) + + if (event.defaultPrevented || !isInteractive) return + + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + openFilePicker() + } + } + + const handleDragEnter = (event: DragEvent) => { + onDragEnter?.(event) + + event.preventDefault() + if (!isInteractive) return + + setIsDragging(true) + } + + const handleDragOver = (event: DragEvent) => { + onDragOver?.(event) + + event.preventDefault() + if (!isInteractive) return + + event.dataTransfer.dropEffect = "copy" + setIsDragging(true) + } + + const handleDragLeave = (event: DragEvent) => { + onDragLeave?.(event) + + if (event.currentTarget.contains(event.relatedTarget as Node)) return + + setIsDragging(false) + } + + const handleDrop = (event: DragEvent) => { + onDrop?.(event) + + event.preventDefault() + setIsDragging(false) + + if (!isInteractive) return + + selectFiles(event.dataTransfer.files) + } + + const message = isDragging + ? "Drop files to add them" + : status === "uploading" + ? normalizedProgress === undefined + ? "Uploading files" + : `Uploading files, ${normalizedProgress}% complete` + : status === "success" + ? multiple + ? "Files are ready" + : "File is ready" + : status === "error" + ? (error ?? "Something went wrong. Try another file.") + : "Drop files here or click to browse" + + return ( +
{ + onClick?.(event) + + if (!event.defaultPrevented) { + openFilePicker() + } + }} + onKeyDown={handleKeyDown} + onDragEnter={handleDragEnter} + onDragLeave={handleDragLeave} + onDragOver={handleDragOver} + onDrop={handleDrop} + {...props} + > + event.stopPropagation()} + onChange={(event) => { + selectFiles(event.target.files) + event.target.value = "" + }} + /> + + + +

{message}

+ {status === "idle" && !isDragging && ( +

+ {multiple ? "Choose one or more files" : "Choose a file"} +

+ )} + {status === "uploading" && normalizedProgress !== undefined && ( +
+
+
+ )} + + {message} + +
+ ) + } +) + +UploadState.displayName = "UploadState" + + +===== EXAMPLE: upload-state-demo ===== +Title: Upload State Demo + +--- file: example/upload-state-demo.tsx --- +"use client" + +import { useEffect, useRef, useState } from "react" + +import { Button } from "@/components/ui/button" +import { + UploadState, + type UploadStateStatus, +} from "@/registry/magicui/upload-state" + +export default function UploadStateDemo() { + const [status, setStatus] = useState("idle") + const [progress, setProgress] = useState(0) + const [fileName, setFileName] = useState() + const uploadTimer = useRef>(undefined) + + const clearUploadTimer = () => { + if (uploadTimer.current) { + clearInterval(uploadTimer.current) + uploadTimer.current = undefined + } + } + + useEffect(() => { + return () => { + if (uploadTimer.current) { + clearInterval(uploadTimer.current) + } + } + }, []) + + const handleFiles = (files: File[]) => { + clearUploadTimer() + setFileName( + files.length === 1 ? files[0].name : `${files.length} files selected` + ) + setProgress(0) + setStatus("uploading") + + let nextProgress = 0 + uploadTimer.current = setInterval(() => { + nextProgress += 10 + setProgress(nextProgress) + + if (nextProgress >= 100) { + clearUploadTimer() + setStatus("success") + } + }, 220) + } + + const reset = () => { + clearUploadTimer() + setStatus("idle") + setProgress(0) + setFileName(undefined) + } + + return ( +
+ +
+

+ {fileName ?? "Choose an image or PDF to start"} +

+
+ + +
+
+
+ ) +} + + + ===== COMPONENT: video-text ===== Title: Video Text Description: A component that displays text with a video playing in the background. diff --git a/apps/www/public/llms.txt b/apps/www/public/llms.txt index a375dac2d..bc9a023e9 100644 --- a/apps/www/public/llms.txt +++ b/apps/www/public/llms.txt @@ -80,6 +80,7 @@ This file provides LLM-friendly entry points to documentation and examples. - [Text Reveal](https://magicui.design/docs/components/text-reveal): Fade in text as you scroll down the page. - [Tweet Card](https://magicui.design/docs/components/tweet-card): A card that displays a tweet with the author's name, handle, and profile picture. - [Typing Animation](https://magicui.design/docs/components/typing-animation): Characters appearing in typed animation +- [Upload State](https://magicui.design/docs/components/upload-state): An accessible file drop area with controlled upload states and progress. - [Video Text](https://magicui.design/docs/components/video-text): A component that displays text with a video playing in the background. - [Warp Background](https://magicui.design/docs/components/warp-background): A card with a time warping background effect. - [Word Rotate](https://magicui.design/docs/components/word-rotate): A vertical rotation of words @@ -219,6 +220,7 @@ This file provides LLM-friendly entry points to documentation and examples. - [Pulsating Button Demo 2](https://github.com/magicuidesign/magicui/blob/main/example/pulsating-button-demo-2.tsx): Example usage - [Ripple Button Demo](https://github.com/magicuidesign/magicui/blob/main/example/ripple-button-demo.tsx): Example usage - [File Tree Demo](https://github.com/magicuidesign/magicui/blob/main/example/file-tree-demo.tsx): Example usage +- [Upload State Demo](https://github.com/magicuidesign/magicui/blob/main/example/upload-state-demo.tsx): Example usage - [Blur Fade Demo](https://github.com/magicuidesign/magicui/blob/main/example/blur-fade-demo.tsx): Example usage - [Blur Fade Text Demo](https://github.com/magicuidesign/magicui/blob/main/example/blur-fade-text-demo.tsx): Example usage - [Safari Demo](https://github.com/magicuidesign/magicui/blob/main/example/safari-demo.tsx): Example usage diff --git a/apps/www/public/r/registry.json b/apps/www/public/r/registry.json index 4662fd09b..f2e67f49e 100644 --- a/apps/www/public/r/registry.json +++ b/apps/www/public/r/registry.json @@ -1131,6 +1131,21 @@ } ] }, + { + "name": "upload-state", + "type": "registry:ui", + "title": "Upload State", + "description": "An accessible file drop area with controlled upload states and progress.", + "dependencies": [ + "lucide-react" + ], + "files": [ + { + "path": "registry/magicui/upload-state.tsx", + "type": "registry:ui" + } + ] + }, { "name": "blur-fade", "type": "registry:ui", @@ -3421,6 +3436,21 @@ } ] }, + { + "name": "upload-state-demo", + "type": "registry:example", + "title": "Upload State Demo", + "description": "Example showing a file drop area with simulated upload progress.", + "registryDependencies": [ + "@magicui/upload-state" + ], + "files": [ + { + "path": "registry/example/upload-state-demo.tsx", + "type": "registry:example" + } + ] + }, { "name": "blur-fade-demo", "type": "registry:example", diff --git a/apps/www/public/r/upload-state-demo.json b/apps/www/public/r/upload-state-demo.json new file mode 100644 index 000000000..4bcff19d2 --- /dev/null +++ b/apps/www/public/r/upload-state-demo.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "upload-state-demo", + "type": "registry:example", + "title": "Upload State Demo", + "description": "Example showing a file drop area with simulated upload progress.", + "registryDependencies": [ + "@magicui/upload-state" + ], + "files": [ + { + "path": "registry/example/upload-state-demo.tsx", + "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n UploadState,\n type UploadStateStatus,\n} from \"@/registry/magicui/upload-state\"\n\nexport default function UploadStateDemo() {\n const [status, setStatus] = useState(\"idle\")\n const [progress, setProgress] = useState(0)\n const [fileName, setFileName] = useState()\n const uploadTimer = useRef>(undefined)\n\n const clearUploadTimer = () => {\n if (uploadTimer.current) {\n clearInterval(uploadTimer.current)\n uploadTimer.current = undefined\n }\n }\n\n useEffect(() => {\n return () => {\n if (uploadTimer.current) {\n clearInterval(uploadTimer.current)\n }\n }\n }, [])\n\n const handleFiles = (files: File[]) => {\n clearUploadTimer()\n setFileName(\n files.length === 1 ? files[0].name : `${files.length} files selected`\n )\n setProgress(0)\n setStatus(\"uploading\")\n\n let nextProgress = 0\n uploadTimer.current = setInterval(() => {\n nextProgress += 10\n setProgress(nextProgress)\n\n if (nextProgress >= 100) {\n clearUploadTimer()\n setStatus(\"success\")\n }\n }, 220)\n }\n\n const reset = () => {\n clearUploadTimer()\n setStatus(\"idle\")\n setProgress(0)\n setFileName(undefined)\n }\n\n return (\n
\n \n
\n

\n {fileName ?? \"Choose an image or PDF to start\"}\n

\n
\n {\n clearUploadTimer()\n setStatus(\"error\")\n }}\n >\n Show error\n \n \n
\n
\n
\n )\n}\n", + "type": "registry:example" + } + ] +} \ No newline at end of file diff --git a/apps/www/public/r/upload-state.json b/apps/www/public/r/upload-state.json new file mode 100644 index 000000000..5eff64ef2 --- /dev/null +++ b/apps/www/public/r/upload-state.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "upload-state", + "type": "registry:ui", + "title": "Upload State", + "description": "An accessible file drop area with controlled upload states and progress.", + "dependencies": [ + "lucide-react" + ], + "files": [ + { + "path": "registry/magicui/upload-state.tsx", + "content": "\"use client\"\n\nimport { forwardRef, useId, useRef, useState } from \"react\"\nimport type { ComponentPropsWithoutRef, DragEvent, KeyboardEvent } from \"react\"\nimport {\n CloudUploadIcon,\n FileCheck2Icon,\n LoaderCircleIcon,\n TriangleAlertIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type UploadStateStatus = \"idle\" | \"uploading\" | \"success\" | \"error\"\n\nexport interface UploadStateProps extends ComponentPropsWithoutRef<\"div\"> {\n accept?: string\n multiple?: boolean\n disabled?: boolean\n status?: UploadStateStatus\n progress?: number\n error?: string\n onFiles?: (files: File[]) => void\n}\n\nexport const UploadState = forwardRef(\n (\n {\n accept,\n multiple = false,\n disabled = false,\n status = \"idle\",\n progress,\n error,\n onFiles,\n className,\n onClick,\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n onKeyDown,\n \"aria-describedby\": ariaDescribedBy,\n ...props\n },\n ref\n ) => {\n const inputRef = useRef(null)\n const [isDragging, setIsDragging] = useState(false)\n const descriptionId = useId()\n const isInteractive = !disabled && status !== \"uploading\"\n const hasProgress =\n typeof progress === \"number\" && Number.isFinite(progress)\n const normalizedProgress = hasProgress\n ? Math.min(100, Math.max(0, progress))\n : undefined\n\n const selectFiles = (files: FileList | null) => {\n const selectedFiles = Array.from(files ?? [])\n\n if (selectedFiles.length > 0) {\n onFiles?.(selectedFiles)\n }\n }\n\n const openFilePicker = () => {\n if (isInteractive) {\n inputRef.current?.click()\n }\n }\n\n const handleKeyDown = (event: KeyboardEvent) => {\n onKeyDown?.(event)\n\n if (event.defaultPrevented || !isInteractive) return\n\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault()\n openFilePicker()\n }\n }\n\n const handleDragEnter = (event: DragEvent) => {\n onDragEnter?.(event)\n\n event.preventDefault()\n if (!isInteractive) return\n\n setIsDragging(true)\n }\n\n const handleDragOver = (event: DragEvent) => {\n onDragOver?.(event)\n\n event.preventDefault()\n if (!isInteractive) return\n\n event.dataTransfer.dropEffect = \"copy\"\n setIsDragging(true)\n }\n\n const handleDragLeave = (event: DragEvent) => {\n onDragLeave?.(event)\n\n if (event.currentTarget.contains(event.relatedTarget as Node)) return\n\n setIsDragging(false)\n }\n\n const handleDrop = (event: DragEvent) => {\n onDrop?.(event)\n\n event.preventDefault()\n setIsDragging(false)\n\n if (!isInteractive) return\n\n selectFiles(event.dataTransfer.files)\n }\n\n const message = isDragging\n ? \"Drop files to add them\"\n : status === \"uploading\"\n ? normalizedProgress === undefined\n ? \"Uploading files\"\n : `Uploading files, ${normalizedProgress}% complete`\n : status === \"success\"\n ? multiple\n ? \"Files are ready\"\n : \"File is ready\"\n : status === \"error\"\n ? (error ?? \"Something went wrong. Try another file.\")\n : \"Drop files here or click to browse\"\n\n return (\n {\n onClick?.(event)\n\n if (!event.defaultPrevented) {\n openFilePicker()\n }\n }}\n onKeyDown={handleKeyDown}\n onDragEnter={handleDragEnter}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n {...props}\n >\n event.stopPropagation()}\n onChange={(event) => {\n selectFiles(event.target.files)\n event.target.value = \"\"\n }}\n />\n\n \n {status === \"uploading\" ? (\n \n ) : status === \"success\" ? (\n \n ) : status === \"error\" ? (\n \n ) : (\n \n )}\n
\n\n

{message}

\n {status === \"idle\" && !isDragging && (\n

\n {multiple ? \"Choose one or more files\" : \"Choose a file\"}\n

\n )}\n {status === \"uploading\" && normalizedProgress !== undefined && (\n \n \n
\n )}\n \n {message}\n \n \n )\n }\n)\n\nUploadState.displayName = \"UploadState\"\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/apps/www/public/registry.json b/apps/www/public/registry.json index 4662fd09b..f2e67f49e 100644 --- a/apps/www/public/registry.json +++ b/apps/www/public/registry.json @@ -1131,6 +1131,21 @@ } ] }, + { + "name": "upload-state", + "type": "registry:ui", + "title": "Upload State", + "description": "An accessible file drop area with controlled upload states and progress.", + "dependencies": [ + "lucide-react" + ], + "files": [ + { + "path": "registry/magicui/upload-state.tsx", + "type": "registry:ui" + } + ] + }, { "name": "blur-fade", "type": "registry:ui", @@ -3421,6 +3436,21 @@ } ] }, + { + "name": "upload-state-demo", + "type": "registry:example", + "title": "Upload State Demo", + "description": "Example showing a file drop area with simulated upload progress.", + "registryDependencies": [ + "@magicui/upload-state" + ], + "files": [ + { + "path": "registry/example/upload-state-demo.tsx", + "type": "registry:example" + } + ] + }, { "name": "blur-fade-demo", "type": "registry:example", diff --git a/apps/www/registry.json b/apps/www/registry.json index 4662fd09b..f2e67f49e 100644 --- a/apps/www/registry.json +++ b/apps/www/registry.json @@ -1131,6 +1131,21 @@ } ] }, + { + "name": "upload-state", + "type": "registry:ui", + "title": "Upload State", + "description": "An accessible file drop area with controlled upload states and progress.", + "dependencies": [ + "lucide-react" + ], + "files": [ + { + "path": "registry/magicui/upload-state.tsx", + "type": "registry:ui" + } + ] + }, { "name": "blur-fade", "type": "registry:ui", @@ -3421,6 +3436,21 @@ } ] }, + { + "name": "upload-state-demo", + "type": "registry:example", + "title": "Upload State Demo", + "description": "Example showing a file drop area with simulated upload progress.", + "registryDependencies": [ + "@magicui/upload-state" + ], + "files": [ + { + "path": "registry/example/upload-state-demo.tsx", + "type": "registry:example" + } + ] + }, { "name": "blur-fade-demo", "type": "registry:example", diff --git a/apps/www/registry/__index__.tsx b/apps/www/registry/__index__.tsx index dfc823153..3f3008733 100644 --- a/apps/www/registry/__index__.tsx +++ b/apps/www/registry/__index__.tsx @@ -1069,6 +1069,23 @@ export const Index: Record = { }), meta: undefined, }, + "upload-state": { + name: "upload-state", + description: "An accessible file drop area with controlled upload states and progress.", + type: "registry:ui", + registryDependencies: undefined, + files: [{ + path: "registry/magicui/upload-state.tsx", + type: "registry:ui", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/magicui/upload-state.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') ?? item.name + return { default: mod.default ?? mod[exportName] } + }), + meta: undefined, + }, "blur-fade": { name: "blur-fade", description: "Blur fade in and out animation. Used to smoothly fade in and out content.", @@ -3585,6 +3602,23 @@ export const Index: Record = { }), meta: undefined, }, + "upload-state-demo": { + name: "upload-state-demo", + description: "Example showing a file drop area with simulated upload progress.", + type: "registry:example", + registryDependencies: ["@magicui/upload-state"], + files: [{ + path: "registry/example/upload-state-demo.tsx", + type: "registry:example", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/example/upload-state-demo.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') ?? item.name + return { default: mod.default ?? mod[exportName] } + }), + meta: undefined, + }, "blur-fade-demo": { name: "blur-fade-demo", description: "Example showing blur fade in and out animations.", diff --git a/apps/www/registry/registry-examples.ts b/apps/www/registry/registry-examples.ts index eb8c8ff10..c73c1f5e4 100644 --- a/apps/www/registry/registry-examples.ts +++ b/apps/www/registry/registry-examples.ts @@ -1784,6 +1784,20 @@ export const examples: Registry["items"] = [ }, ], }, + { + name: "upload-state-demo", + type: "registry:example", + title: "Upload State Demo", + description: + "Example showing a file drop area with simulated upload progress.", + registryDependencies: ["@magicui/upload-state"], + files: [ + { + path: "example/upload-state-demo.tsx", + type: "registry:example", + }, + ], + }, { name: "blur-fade-demo", type: "registry:example", diff --git a/apps/www/registry/registry-ui.ts b/apps/www/registry/registry-ui.ts index d42e6a097..fb587d78b 100644 --- a/apps/www/registry/registry-ui.ts +++ b/apps/www/registry/registry-ui.ts @@ -1087,6 +1087,20 @@ export const ui: Registry["items"] = [ }, ], }, + { + name: "upload-state", + type: "registry:ui", + title: "Upload State", + description: + "An accessible file drop area with controlled upload states and progress.", + dependencies: ["lucide-react"], + files: [ + { + path: "magicui/upload-state.tsx", + type: "registry:ui", + }, + ], + }, { name: "blur-fade", type: "registry:ui",