{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upload",
  "title": "Upload",
  "author": "AlgorizeTech",
  "description": "Ask the user to pick one of 3 file upload variants: https://avaraui.com/view/file-upload. This is the Dropzone with upload states. Skip the question if the user already named one. A drag-and-drop file dropzone with per-file progress, previews, retry and time-remaining. Use it for multi-file uploads where users need to watch each file land.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/UploadBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  AlertTriangle,\n  Check,\n  FileText,\n  RefreshCw,\n  UploadCloud,\n  X,\n} from \"lucide-react\";\nimport React, { forwardRef, useEffect, useRef, useState } from \"react\";\nimport { formatBytes, formatTimeLeft } from \"./upload-format\";\n\n// ==========================================\n// TYPES, FORMATTERS & THEME ENGINE\n// ==========================================\nexport type UploadStatus =\n  | \"idle\"\n  | \"uploading\"\n  | \"paused\"\n  | \"error\"\n  | \"success\"\n  | (string & {});\n\nexport interface FileUploadState {\n  id: string;\n  file: File;\n  progress: number;\n  status: UploadStatus;\n  timeLeft?: number; // in seconds\n  errorMessage?: string;\n  previewUrl?: string;\n}\n\n// ==========================================\n// MOTION (library-free)\n// ==========================================\n// GSAP's back.out(2), power2.out and power1.out as CSS easings.\nconst BACK_OUT_2 =\n  \"linear(0, .2225, .4153, .5802, .7191, .8341, .9271, 1, 1.0547, 1.0933, 1.1176, 1.1296, 1.1312, 1.1244, 1.1111, 1.0933, 1.0729, 1.0518, 1.0321, 1.0155, 1.0042, 1)\";\nconst POWER2_OUT = \"cubic-bezier(.3333, 1, .6667, 1)\";\nconst POWER1_OUT = \"cubic-bezier(.3333, .6667, .6667, 1)\";\n\nconst running = new WeakMap<HTMLElement, Animation>();\n\n/**\n * Tween `el` to `to` from wherever it is right now (mid-flight included), so a\n * new target takes over a running tween with the full duration and its own\n * ease. The end values are written inline, so nothing is held by a fill.\n */\nfunction tweenTo(el: HTMLElement, to: Record<string, string>, duration: number, easing: string) {\n  const computed = getComputedStyle(el);\n  const from: Record<string, string> = {};\n  for (const key of Object.keys(to)) from[key] = computed.getPropertyValue(key);\n  running.get(el)?.cancel();\n  for (const [key, value] of Object.entries(to)) el.style.setProperty(key, value);\n  if (typeof el.animate !== \"function\") return;\n  running.set(el, el.animate([from, to], { duration: duration * 1000, easing }));\n}\n\n// Accent colour comes from the `--primary` theme token (`border-primary/30`,\n// `bg-primary`, `text-primary`), so the dropzone and the progress bar follow\n// the site theme and the docs colour picker with no prop and no polling.\n\n// ==========================================\n// 1. DROPZONE (Glitch-Free & Theme Adaptive)\n// ==========================================\nexport interface DropzoneProps extends React.HTMLAttributes<HTMLDivElement> {\n  onFilesDrop: (files: File[]) => void;\n  maxSize?: number; // in MB\n  accept?: string;\n}\n\nexport const Dropzone = forwardRef<HTMLDivElement, DropzoneProps>(\n  ({ onFilesDrop, maxSize = 50, accept, className, ...props }, ref) => {\n    const internalRef = useRef<HTMLDivElement>(null);\n    const iconRef = useRef<HTMLDivElement>(null);\n    const textRef = useRef<HTMLDivElement>(null);\n    const fileInputRef = useRef<HTMLInputElement>(null);\n    const [isDragging, setIsDragging] = useState(false);\n\n    // Drag counter prevents bubbling glitches when hovering over children\n    const dragCounter = useRef(0);\n\n    React.useImperativeHandle(ref, () => internalRef.current as HTMLDivElement);\n\n    // The icon and text lift on drag (Web Animations on `translate`/`scale`);\n    // the colours (border, tint, glow, icon) are Tailwind `data-[dragging]`\n    // classes on the token.\n    useEffect(() => {\n      if (!iconRef.current || !textRef.current) return;\n\n      if (isDragging) {\n        tweenTo(iconRef.current, { translate: \"0px -8px\", scale: \"1.1\" }, 0.4, BACK_OUT_2);\n        tweenTo(textRef.current, { translate: \"0px -4px\" }, 0.4, BACK_OUT_2);\n      } else {\n        tweenTo(iconRef.current, { translate: \"0px 0px\", scale: \"1\" }, 0.4, POWER2_OUT);\n        tweenTo(textRef.current, { translate: \"0px 0px\" }, 0.4, POWER2_OUT);\n      }\n    }, [isDragging]);\n\n    const handleDragEnter = (e: React.DragEvent) => {\n      e.preventDefault();\n      e.stopPropagation();\n      dragCounter.current += 1;\n      if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {\n        setIsDragging(true);\n      }\n    };\n\n    const handleDragLeave = (e: React.DragEvent) => {\n      e.preventDefault();\n      e.stopPropagation();\n      dragCounter.current -= 1;\n      if (dragCounter.current === 0) {\n        setIsDragging(false);\n      }\n    };\n\n    const handleDragOver = (e: React.DragEvent) => {\n      e.preventDefault();\n      e.stopPropagation();\n    };\n\n    const handleDrop = (e: React.DragEvent) => {\n      e.preventDefault();\n      e.stopPropagation();\n      setIsDragging(false);\n      dragCounter.current = 0;\n      if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n        onFilesDrop(Array.from(e.dataTransfer.files));\n      }\n    };\n\n    const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {\n      if (e.target.files && e.target.files.length > 0) {\n        onFilesDrop(Array.from(e.target.files));\n        e.target.value = \"\"; // Reset to allow re-selecting same file\n      }\n    };\n\n    return (\n      <div\n        ref={internalRef}\n        onDragEnter={handleDragEnter}\n        onDragLeave={handleDragLeave}\n        onDragOver={handleDragOver}\n        onDrop={handleDrop}\n        onClick={() => fileInputRef.current?.click()}\n        data-dragging={isDragging || undefined}\n        className={cn(\n          \"relative w-full rounded-2xl border-2 border-dashed border-border flex flex-col items-center justify-center py-8 sm:py-12 px-4 sm:px-6 cursor-pointer overflow-hidden group bg-card/50\",\n          \"transition-[border-color,background-color,box-shadow] duration-300\",\n          \"data-[dragging]:border-primary/30 data-[dragging]:bg-primary/5 data-[dragging]:shadow-[0_0_30px_-5px_color-mix(in_oklab,var(--primary)_20%,transparent)]\",\n          className,\n        )}\n        {...props}\n      >\n        <input\n          type=\"file\"\n          ref={fileInputRef}\n          onChange={handleFileSelect}\n          multiple\n          accept={accept}\n          className=\"hidden\"\n        />\n\n        {/* pointer-events-none prevents drag events from bubbling and glitching the hover state */}\n        <div\n          ref={iconRef}\n          className=\"text-muted-foreground mb-3 sm:mb-4 transition-colors duration-300 pointer-events-none group-data-[dragging]:text-primary\"\n        >\n          <UploadCloud className=\"w-8 h-8 sm:w-10 sm:h-10\" strokeWidth={1.5} />\n        </div>\n\n        <div\n          ref={textRef}\n          className=\"flex flex-col items-center text-center pointer-events-none px-2\"\n        >\n          <h3 className=\"text-sm sm:text-base font-semibold text-foreground\">\n            {isDragging\n              ? \"Release to upload\"\n              : \"Drop your files here or click to browse\"}\n          </h3>\n          <p className=\"text-xs sm:text-[13px] text-muted-foreground mt-1\">\n            PNG, JPG, PDF, MP4 — up to {maxSize} MB\n          </p>\n        </div>\n      </div>\n    );\n  },\n);\nDropzone.displayName = \"Dropzone\";\n\n// ==========================================\n// 2. UPLOAD ITEM (Notification Style + Spotlight)\n// ==========================================\n// Registered so the spotlight's position and colour interpolate.\nconst SPOTLIGHT_CSS = `\n@property --upload-spot-x { syntax: \"<length-percentage>\"; inherits: false; initial-value: 50%; }\n@property --upload-spot-y { syntax: \"<length-percentage>\"; inherits: false; initial-value: 50%; }\n@property --upload-spot-color { syntax: \"<color>\"; inherits: false; initial-value: rgba(0, 0, 0, 0); }\n`;\n\nexport interface UploadItemProps {\n  item: FileUploadState;\n  onRetry?: (id: string) => void;\n  onRemove?: (id: string) => void;\n}\n\nexport const UploadItem = forwardRef<HTMLDivElement, UploadItemProps>(\n  ({ item, onRetry, onRemove }, ref) => {\n    const { file, progress, status, timeLeft, errorMessage, previewUrl } = item;\n\n    const cardRef = useRef<HTMLDivElement>(null);\n    const progressRef = useRef<HTMLDivElement>(null);\n    const glowRef = useRef<HTMLDivElement>(null);\n\n    React.useImperativeHandle(ref, () => cardRef.current as HTMLDivElement);\n\n    // Smooth Progress Bar Animation\n    useEffect(() => {\n      if (progressRef.current) {\n        tweenTo(progressRef.current, { width: `${progress}%` }, 0.3, POWER2_OUT);\n      }\n    }, [progress]);\n\n    // Mouse-tracking Spotlight Effect (Adaptive for Light/Dark Mode). The\n    // gradient reads registered custom properties (see SPOTLIGHT_CSS), which\n    // CSS can transition where it cannot transition a gradient itself.\n    const moveSpotlight = (x: string, y: string, color: string, duration: number) => {\n      const glow = glowRef.current;\n      if (!glow) return;\n      glow.style.transition = [\n        \"opacity 300ms cubic-bezier(0.4, 0, 0.2, 1)\",\n        ...[\"--upload-spot-x\", \"--upload-spot-y\", \"--upload-spot-color\"].map(\n          (property) => `${property} ${duration}s ${POWER1_OUT}`,\n        ),\n      ].join(\", \");\n      glow.style.setProperty(\"--upload-spot-x\", x);\n      glow.style.setProperty(\"--upload-spot-y\", y);\n      glow.style.setProperty(\"--upload-spot-color\", color);\n    };\n\n    const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {\n      if (!cardRef.current || !glowRef.current) return;\n      const rect = cardRef.current.getBoundingClientRect();\n      const x = e.clientX - rect.left;\n      const y = e.clientY - rect.top;\n\n      const isDark = document.documentElement.classList.contains(\"dark\");\n      const spotlightColor = isDark\n        ? \"rgba(255,255,255,0.06)\"\n        : \"rgba(0,0,0,0.04)\";\n\n      moveSpotlight(`${x}px`, `${y}px`, spotlightColor, 0.1);\n    };\n\n    const handleMouseLeave = () => {\n      moveSpotlight(\"50%\", \"50%\", \"rgba(0,0,0,0)\", 0.5);\n    };\n\n    const isImage = file.type.startsWith(\"image/\");\n    const isError = status === \"error\";\n    const isSuccess = status === \"success\";\n    const isPaused = status === \"paused\";\n\n    // Determine structural colors based on state\n    const barColorClass = isError\n      ? \"bg-rose-500\"\n      : isSuccess\n        ? \"bg-emerald-500\"\n        : \"bg-primary\";\n\n    const iconColor = isError\n      ? \"text-rose-500\"\n      : isSuccess\n        ? \"text-emerald-500\"\n        : \"text-muted-foreground\";\n\n    // Extract file extension for badge\n    const fileExtension = file.name.split(\".\").pop()?.toUpperCase() || \"FILE\";\n\n    return (\n      <div\n        ref={cardRef}\n        onMouseMove={handleMouseMove}\n        onMouseLeave={handleMouseLeave}\n        className={cn(\n          \"w-full min-w-0 bg-card text-card-foreground border rounded-2xl p-3.5 sm:p-4 flex flex-col gap-3 sm:gap-4 relative overflow-hidden transition-colors duration-300 shadow-[0_2px_12px_rgba(0,0,0,0.03)] dark:shadow-[0_2px_20px_rgba(0,0,0,0.2)] group\",\n          isError\n            ? \"border-rose-200 dark:border-rose-900/50\"\n            : isSuccess\n              ? \"border-emerald-200 dark:border-emerald-900/30\"\n              : \"border-border\",\n        )}\n      >\n        {/* Spotlight Overlay Layer */}\n        <style>{SPOTLIGHT_CSS}</style>\n        <div\n          ref={glowRef}\n          className=\"absolute inset-0 z-0 pointer-events-none transition-opacity duration-300 opacity-0 group-hover:opacity-100\"\n          style={{\n            background:\n              \"radial-gradient(600px circle at var(--upload-spot-x) var(--upload-spot-y), var(--upload-spot-color), transparent 40%)\",\n          }}\n        />\n\n        <div className=\"flex items-start gap-3 sm:gap-4 relative z-10 w-full min-w-0\">\n          {/* Signal 04: Upload Preview */}\n          <div\n            className={cn(\n              \"rounded-xl bg-muted border border-border flex items-center justify-center shrink-0 overflow-hidden relative shadow-sm\",\n              previewUrl && isImage ? \"w-12 h-12 sm:w-14 sm:h-14\" : \"w-10 h-10 sm:w-12 sm:h-12\",\n            )}\n          >\n            {previewUrl && isImage ? (\n              <img\n                src={previewUrl}\n                alt=\"preview\"\n                className=\"w-full h-full object-cover\"\n              />\n            ) : (\n              <FileText className={cn(\"w-5 h-5\", iconColor)} />\n            )}\n\n            {/* Success Overlay Badge */}\n            {isSuccess && (\n              <div className=\"absolute inset-0 bg-emerald-500/20 backdrop-blur-[2px] flex items-center justify-center\">\n                <Check\n                  className=\"w-5 h-5 text-emerald-600 dark:text-emerald-400 drop-shadow-md\"\n                  strokeWidth={3}\n                />\n              </div>\n            )}\n          </div>\n\n          <div className=\"flex-1 min-w-0 flex flex-col justify-center pt-0.5\">\n            <div className=\"flex items-center justify-between gap-2 sm:gap-4\">\n              <h4 className=\"text-[13px] sm:text-[14px] font-semibold text-foreground truncate pr-1\">\n                {file.name}\n              </h4>\n              <span className=\"text-[12px] sm:text-[13px] font-semibold text-foreground shrink-0\">\n                {Math.round(progress)}%\n              </span>\n            </div>\n\n            {/* Badges / File Info */}\n            <div className=\"flex items-center gap-2 mt-0.5 sm:mt-1 mb-1.5 sm:mb-2\">\n              <span className=\"bg-muted text-muted-foreground px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wider border border-border dark:border-transparent\">\n                {fileExtension}\n              </span>\n              <span className=\"text-[11px] font-semibold text-muted-foreground\">\n                {formatBytes(file.size)}\n              </span>\n            </div>\n\n            {/* Honest Progress & Inline Retry Messaging */}\n            <div className=\"flex items-center justify-between gap-2 min-h-[24px] mt-auto\">\n              <div className=\"flex items-center gap-1.5 min-w-0 text-[11px] sm:text-[12px] font-semibold text-muted-foreground overflow-hidden\">\n                {isError ? (\n                  <span className=\"flex items-center text-rose-500 gap-1.5 truncate\">\n                    <AlertTriangle className=\"w-3.5 h-3.5 shrink-0\" />\n                    <span className=\"truncate\">{errorMessage || \"Connection lost\"}</span>\n                  </span>\n                ) : isSuccess ? (\n                  <span className=\"flex items-center text-emerald-600 dark:text-emerald-500 gap-1.5 truncate\">\n                    <Check className=\"w-3.5 h-3.5 shrink-0\" />\n                    <span className=\"truncate\">Upload complete</span>\n                  </span>\n                ) : (\n                  <span className=\"truncate\">{formatTimeLeft(timeLeft)}</span>\n                )}\n              </div>\n\n              {/* Actions */}\n              <div className=\"flex items-center gap-1.5 sm:gap-2 shrink-0\">\n                {(isError || isPaused) && onRetry && (\n                  <button\n                    onClick={() => onRetry(item.id)}\n                    className=\"flex items-center gap-1 sm:gap-1.5 px-2 py-1 rounded-md bg-muted hover:bg-muted/80 text-foreground text-[11px] font-semibold transition-colors border border-border dark:border-transparent cursor-pointer\"\n                  >\n                    <RefreshCw className=\"w-3 h-3 shrink-0\" /> Retry\n                  </button>\n                )}\n                {onRemove && (\n                  <button\n                    onClick={() => onRemove(item.id)}\n                    className=\"p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors cursor-pointer\"\n                    aria-label=\"Remove file\"\n                  >\n                    <X className=\"w-4 h-4\" />\n                  </button>\n                )}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Progress Bar Track */}\n        <div className=\"w-full h-1.5 bg-muted rounded-full overflow-hidden relative z-10\">\n          <div\n            ref={progressRef}\n            className={cn(\n              \"absolute top-0 bottom-0 left-0 rounded-full\",\n              barColorClass,\n            )}\n            style={{ width: \"0%\" }}\n          />\n        </div>\n      </div>\n    );\n  },\n);\nUploadItem.displayName = \"UploadItem\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/UploadBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/upload-format.ts",
      "content": "\"use client\";\n\nexport function formatBytes(bytes: number, decimals = 2) {\n  if (!+bytes) return \"0 Bytes\";\n  const k = 1024;\n  const dm = decimals < 0 ? 0 : decimals;\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;\n}\n\nexport function formatTimeLeft(seconds?: number) {\n  if (seconds === undefined || seconds < 0) return \"Calculating...\";\n  if (seconds === 0) return \"Almost done...\";\n  if (seconds < 60) return `${seconds}s left`;\n  return `${Math.floor(seconds / 60)}m ${seconds % 60}s left`;\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/upload-format.ts"
    }
  ],
  "docs": "Named exports: Dropzone, UploadItem, formatBytes, formatTimeLeft. Dropzone handles the drop target and reports via `onFilesDrop`; render an UploadItem per file and drive its `status` and `progress` yourself. No transport is included — wire it to your own upload call.",
  "categories": [
    "forms",
    "feedback"
  ],
  "type": "registry:component"
}
