{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "file-upload.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowUpRight,\n  Check,\n  FileText,\n  ImageIcon,\n  Music,\n  Plus,\n  RefreshCw,\n  Video,\n  X,\n} from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type Ref,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\nconst controlCornerClassName =\n  \"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]\";\n\nexport type FileUploadStatus = \"uploading\" | \"done\" | \"error\";\n\nexport type FileUploadRejectReason =\n  | \"accept\"\n  | \"max-size\"\n  | \"max-files\"\n  | \"duplicate\"\n  | \"validation\"\n  | \"disabled\";\n\nexport type FileUploadItem = {\n  id: string;\n  file: File;\n  preview?: string;\n  progress: number;\n  status: FileUploadStatus;\n  error?: string;\n};\n\nexport type FileUploadUploadContext = {\n  setProgress: (progress: number) => void;\n};\n\nexport interface FileUploadProps {\n  accept?: string;\n  ariaDescribedBy?: string;\n  ariaLabel?: string;\n  browseLabel?: string;\n  className?: string;\n  clearAllLabel?: string;\n  defaultValue?: File[];\n  description?: string;\n  disabled?: boolean;\n  dropzoneDescription?: string;\n  dropzoneTitle?: string;\n  id?: string;\n  invalid?: boolean;\n  libraryLabel?: string;\n  maxFiles?: number;\n  maxSize?: number;\n  multiple?: boolean;\n  name?: string;\n  onFileRemove?: (file: File, nextFiles: File[]) => void;\n  onFilesChange?: (files: File[]) => void;\n  onReject?: (\n    files: File[],\n    reason: FileUploadRejectReason,\n    message: string\n  ) => void;\n  onUpload?: (file: File, context: FileUploadUploadContext) => Promise<void>;\n  onUploadComplete?: (files: File[]) => void;\n  onValueChange?: (items: FileUploadItem[]) => void;\n  preventDuplicates?: boolean;\n  required?: boolean;\n  showClearAll?: boolean;\n  simulateUpload?: boolean;\n  validateFile?: (file: File) => boolean | string;\n  value?: FileUploadItem[];\n}\n\nconst clampProgress = (value: number) => {\n  if (!Number.isFinite(value)) return 0;\n  return Math.min(100, Math.max(0, value));\n};\n\nconst formatBytes = (bytes: number) => {\n  if (!Number.isFinite(bytes) || bytes < 0) return \"0 B\";\n  if (bytes < 1024) return `${bytes} B`;\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n};\n\nconst resolveMaxFiles = (maxFiles?: number) => {\n  if (typeof maxFiles !== \"number\" || !Number.isFinite(maxFiles))\n    return undefined;\n  return Math.max(0, Math.floor(maxFiles));\n};\n\nconst resolveMaxSize = (maxSize?: number) => {\n  if (\n    typeof maxSize !== \"number\" ||\n    !Number.isFinite(maxSize) ||\n    maxSize <= 0\n  ) {\n    return undefined;\n  }\n  return maxSize;\n};\n\nconst buildFileId = (file: File) =>\n  `${file.name}-${file.size}-${file.lastModified}-${Math.random().toString(36).slice(2, 8)}`;\n\nconst fileSignature = (file: File) =>\n  `${file.name}:${file.size}:${file.lastModified}`;\n\nconst matchesAccept = (file: File, accept?: string) => {\n  if (!accept?.trim()) return true;\n\n  const fileName = file.name.toLowerCase();\n  const fileType = file.type.toLowerCase();\n\n  return accept\n    .split(\",\")\n    .map((entry) => entry.trim().toLowerCase())\n    .some((entry) => {\n      if (!entry) return false;\n      if (entry.endsWith(\"/*\")) {\n        const prefix = entry.slice(0, -1);\n        return fileType.startsWith(prefix);\n      }\n      if (entry.startsWith(\".\")) {\n        return fileName.endsWith(entry);\n      }\n      if (entry.includes(\"/\")) {\n        return fileType === entry;\n      }\n      return fileName.endsWith(`.${entry}`) || fileType === entry;\n    });\n};\n\nconst formatAcceptHint = (accept: string) =>\n  accept\n    .split(\",\")\n    .map((entry) => entry.trim())\n    .filter(Boolean)\n    .join(\", \");\n\nconst buildDropzoneHint = ({\n  accept,\n  maxFiles,\n  maxSize,\n}: {\n  accept?: string;\n  maxFiles?: number;\n  maxSize?: number;\n}) => {\n  const parts: string[] = [];\n\n  if (accept?.trim()) {\n    parts.push(formatAcceptHint(accept));\n  } else {\n    parts.push(\"Images, videos, audio, or documents\");\n  }\n\n  if (typeof maxSize === \"number\") {\n    parts.push(`Max ${formatBytes(maxSize)} per file`);\n  }\n\n  if (typeof maxFiles === \"number\") {\n    parts.push(maxFiles === 1 ? \"1 file only\" : `Up to ${maxFiles} files`);\n  }\n\n  return parts.join(\" · \");\n};\n\nconst createPreviewUrl = (file: File) => {\n  if (file.type.startsWith(\"image/\") || file.type.startsWith(\"video/\")) {\n    return URL.createObjectURL(file);\n  }\n  return undefined;\n};\n\nconst createUploadItem = (file: File): FileUploadItem => ({\n  id: buildFileId(file),\n  file,\n  preview: createPreviewUrl(file),\n  progress: 0,\n  status: \"uploading\",\n});\n\nconst revokePreview = (item: FileUploadItem) => {\n  if (!item.preview) return;\n  URL.revokeObjectURL(item.preview);\n  item.preview = undefined;\n};\n\nconst revokeAllPreviews = (items: FileUploadItem[]) => {\n  for (const item of items) revokePreview(item);\n};\n\nconst kindOf = (file: File) => {\n  if (file.type.startsWith(\"image/\")) return \"image\";\n  if (file.type.startsWith(\"video/\")) return \"video\";\n  if (file.type.startsWith(\"audio/\")) return \"audio\";\n  return \"doc\";\n};\n\nconst setRef = <T,>(ref: Ref<T> | undefined, value: T | null) => {\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n  if (ref) {\n    ref.current = value;\n  }\n};\n\nfunction syncInputFiles(input: HTMLInputElement, items: FileUploadItem[]) {\n  try {\n    const dt = new DataTransfer();\n    for (const item of items) {\n      if (item.status !== \"error\") {\n        dt.items.add(item.file);\n      }\n    }\n    input.files = dt.files;\n  } catch {\n    // DataTransfer assignment can fail in unsupported environments.\n  }\n}\n\nfunction startUploadRun(runUpload: (id: string) => Promise<void>, id: string) {\n  runUpload(id).catch(() => undefined);\n}\n\nfunction getAvailableSlots(\n  multiple: boolean,\n  maxFiles: number | undefined,\n  currentCount: number\n) {\n  if (!multiple) return maxFiles === 0 ? 0 : 1;\n  if (typeof maxFiles === \"number\") return Math.max(maxFiles - currentCount, 0);\n  return Number.POSITIVE_INFINITY;\n}\n\ntype IncomingValidation = {\n  accepted: File[];\n  rejected: File[];\n  rejectionMessages: string[];\n  reason: FileUploadRejectReason;\n};\n\nfunction runValidateFile(\n  file: File,\n  validateFile?: (file: File) => boolean | string\n) {\n  if (!validateFile) return true as const;\n\n  try {\n    return validateFile(file);\n  } catch {\n    return `${file.name} failed validation`;\n  }\n}\n\ntype IncomingFileRejection = {\n  reason: FileUploadRejectReason;\n  message: string;\n};\n\nfunction getIncomingFileRejection(\n  file: File,\n  {\n    accept,\n    maxSize,\n    validateFile,\n    preventDuplicates,\n    existingSignatures,\n  }: {\n    accept?: string;\n    maxSize?: number;\n    validateFile?: (file: File) => boolean | string;\n    preventDuplicates: boolean;\n    existingSignatures: Set<string>;\n  }\n): IncomingFileRejection | null {\n  if (!matchesAccept(file, accept)) {\n    return {\n      reason: \"accept\",\n      message: `${file.name} is not an accepted file type`,\n    };\n  }\n\n  if (typeof maxSize === \"number\" && file.size > maxSize) {\n    return {\n      reason: \"max-size\",\n      message: `${file.name} exceeds the ${formatBytes(maxSize)} limit`,\n    };\n  }\n\n  const validationResult = runValidateFile(file, validateFile);\n  if (validationResult !== true) {\n    return {\n      reason: \"validation\",\n      message:\n        typeof validationResult === \"string\"\n          ? validationResult\n          : `${file.name} failed validation`,\n    };\n  }\n\n  const signature = fileSignature(file);\n  if (preventDuplicates && existingSignatures.has(signature)) {\n    return {\n      reason: \"duplicate\",\n      message: `${file.name} is already in the queue`,\n    };\n  }\n\n  return null;\n}\n\nfunction validateIncomingFiles({\n  accept,\n  currentCount,\n  files,\n  maxFiles,\n  maxSize,\n  multiple,\n  preventDuplicates,\n  existingSignatures,\n  validateFile,\n}: {\n  accept?: string;\n  currentCount: number;\n  files: File[];\n  maxFiles?: number;\n  maxSize?: number;\n  multiple: boolean;\n  preventDuplicates: boolean;\n  existingSignatures: Set<string>;\n  validateFile?: (file: File) => boolean | string;\n}): IncomingValidation {\n  const accepted: File[] = [];\n  const rejected: File[] = [];\n  const rejectionMessages: string[] = [];\n  let reason: FileUploadRejectReason = \"validation\";\n  const availableSlots = getAvailableSlots(multiple, maxFiles, currentCount);\n  const rejectionOptions = {\n    accept,\n    maxSize,\n    validateFile,\n    preventDuplicates,\n    existingSignatures,\n  };\n\n  for (const file of files) {\n    if (accepted.length >= availableSlots) {\n      rejected.push(file);\n      reason = \"max-files\";\n      rejectionMessages.push(\n        typeof maxFiles === \"number\"\n          ? `Maximum of ${maxFiles} file${maxFiles === 1 ? \"\" : \"s\"} reached`\n          : \"File limit reached\"\n      );\n      continue;\n    }\n\n    const rejection = getIncomingFileRejection(file, rejectionOptions);\n    if (rejection) {\n      rejected.push(file);\n      reason = rejection.reason;\n      rejectionMessages.push(rejection.message);\n      continue;\n    }\n\n    accepted.push(file);\n    existingSignatures.add(fileSignature(file));\n  }\n\n  return { accepted, rejected, rejectionMessages, reason };\n}\n\nfunction buildNextQueue({\n  acceptedFiles,\n  currentItems,\n  maxFiles,\n  multiple,\n}: {\n  acceptedFiles: File[];\n  currentItems: FileUploadItem[];\n  maxFiles?: number;\n  multiple: boolean;\n}) {\n  const createdItems = acceptedFiles.map(createUploadItem);\n  const merged = multiple\n    ? [...createdItems, ...currentItems]\n    : createdItems.slice(0, 1);\n  const nextItems =\n    typeof maxFiles === \"number\" ? merged.slice(0, maxFiles) : merged;\n  const keptIds = new Set(nextItems.map((item) => item.id));\n\n  for (const item of currentItems) {\n    if (!keptIds.has(item.id)) revokePreview(item);\n  }\n\n  for (const item of createdItems) {\n    if (!keptIds.has(item.id)) revokePreview(item);\n  }\n\n  return { createdItems, keptIds, nextItems };\n}\n\nfunction shouldAbortUpload(\n  id: string,\n  uploadCancelledRef: RefObject<Set<string>>,\n  itemIsQueued: (id: string) => boolean,\n  mountedRef: RefObject<boolean>\n) {\n  return (\n    uploadCancelledRef.current.has(id) ||\n    !itemIsQueued(id) ||\n    !mountedRef.current\n  );\n}\n\nfunction simulateUploadTick(items: FileUploadItem[]) {\n  let changed = false;\n  const next = items.map((item) => {\n    if (item.status !== \"uploading\") return item;\n    changed = true;\n    const progress = Math.min(100, item.progress + Math.random() * 18 + 6);\n    if (progress >= 100) {\n      return { ...item, progress: 100, status: \"done\" as const };\n    }\n    return { ...item, progress };\n  });\n  return changed ? next : items;\n}\n\nconst KindIcon = ({ kind }: { kind: string }) => {\n  const cls = \"h-4 w-4\";\n  if (kind === \"image\") return <ImageIcon className={cls} />;\n  if (kind === \"video\") return <Video className={cls} />;\n  if (kind === \"audio\") return <Music className={cls} />;\n  return <FileText className={cls} />;\n};\n\nfunction ProgressRing({\n  progress,\n  size = 44,\n  label,\n  reduceMotion,\n}: {\n  progress: number;\n  size?: number;\n  label: string;\n  reduceMotion: boolean;\n}) {\n  const stroke = 2.5;\n  const r = (size - stroke) / 2;\n  const c = 2 * Math.PI * r;\n  const offset = c - (clampProgress(progress) / 100) * c;\n  const rounded = Math.round(clampProgress(progress));\n  const transition: Transition = reduceMotion\n    ? { duration: 0 }\n    : { type: \"spring\", stiffness: 80, damping: 20 };\n\n  return (\n    <svg\n      aria-label={label}\n      aria-valuemax={100}\n      aria-valuemin={0}\n      aria-valuenow={rounded}\n      className=\"-rotate-90\"\n      height={size}\n      role=\"progressbar\"\n      width={size}\n    >\n      <circle\n        cx={size / 2}\n        cy={size / 2}\n        fill=\"none\"\n        r={r}\n        stroke=\"var(--color-border)\"\n        strokeWidth={stroke}\n      />\n      <motion.circle\n        animate={{ strokeDashoffset: offset }}\n        cx={size / 2}\n        cy={size / 2}\n        fill=\"none\"\n        r={r}\n        stroke=\"color-mix(in oklab, var(--color-foreground) 26%, transparent)\"\n        strokeDasharray={c}\n        strokeLinecap=\"round\"\n        strokeWidth={stroke}\n        transition={transition}\n      />\n    </svg>\n  );\n}\n\nasync function executeFileUpload({\n  announce,\n  id,\n  itemIsQueued,\n  mountedRef,\n  onUpload,\n  updateFileItem,\n  uploadCancelledRef,\n  uploadRunRef,\n  file,\n}: {\n  announce: (message: string) => void;\n  id: string;\n  itemIsQueued: (id: string) => boolean;\n  mountedRef: RefObject<boolean>;\n  onUpload: (file: File, context: FileUploadUploadContext) => Promise<void>;\n  updateFileItem: (id: string, patch: Partial<FileUploadItem>) => void;\n  uploadCancelledRef: RefObject<Set<string>>;\n  uploadRunRef: RefObject<Set<string>>;\n  file: File;\n}) {\n  uploadCancelledRef.current.delete(id);\n  uploadRunRef.current.add(id);\n  updateFileItem(id, { status: \"uploading\", progress: 0, error: undefined });\n\n  try {\n    await onUpload(file, {\n      setProgress: (progress) => {\n        if (\n          shouldAbortUpload(id, uploadCancelledRef, itemIsQueued, mountedRef)\n        ) {\n          return;\n        }\n        updateFileItem(id, { progress: clampProgress(progress) });\n      },\n    });\n\n    if (shouldAbortUpload(id, uploadCancelledRef, itemIsQueued, mountedRef)) {\n      return;\n    }\n\n    updateFileItem(id, { status: \"done\", progress: 100 });\n    announce(`${file.name} upload complete`);\n  } catch (error) {\n    if (shouldAbortUpload(id, uploadCancelledRef, itemIsQueued, mountedRef)) {\n      return;\n    }\n\n    const message = error instanceof Error ? error.message : \"Upload failed\";\n    updateFileItem(id, { status: \"error\", error: message, progress: 0 });\n    announce(`${file.name} upload failed`);\n  } finally {\n    uploadRunRef.current.delete(id);\n  }\n}\n\ntype FileUploadDropzoneProps = {\n  accept?: string;\n  ariaDescribedBy?: string;\n  ariaLabel: string;\n  atMaxFiles: boolean;\n  browseLabel: string;\n  disabled: boolean;\n  dropzoneDescription: string;\n  dropzoneTitle: string;\n  filesCount: number;\n  hintId: string;\n  inputRef: RefObject<HTMLInputElement | null>;\n  isDragging: boolean;\n  maxFiles?: number;\n  multiple: boolean;\n  name?: string;\n  onBrowse: () => void;\n  onDragEnter: (event: React.DragEvent) => void;\n  onDragLeave: (event: React.DragEvent) => void;\n  onDrop: (event: React.DragEvent) => void;\n  onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;\n  onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;\n  prefersReducedMotion: boolean;\n  required: boolean;\n  rootId: string;\n  shellTransition: Transition;\n  showError: boolean;\n};\n\nfunction FileUploadDropzone({\n  accept,\n  ariaDescribedBy,\n  ariaLabel,\n  atMaxFiles,\n  browseLabel,\n  disabled,\n  dropzoneDescription,\n  dropzoneTitle,\n  filesCount,\n  hintId,\n  inputRef,\n  isDragging,\n  maxFiles,\n  multiple,\n  name,\n  onBrowse,\n  onDragEnter,\n  onDragLeave,\n  onDrop,\n  onInputChange,\n  onKeyDown,\n  prefersReducedMotion,\n  required,\n  rootId,\n  shellTransition,\n  showError,\n}: FileUploadDropzoneProps) {\n  const title =\n    atMaxFiles && !isDragging\n      ? `Maximum of ${maxFiles} file${maxFiles === 1 ? \"\" : \"s\"} reached`\n      : dropzoneTitle;\n\n  return (\n    <motion.div\n      animate={{\n        scale: isDragging && !disabled && !prefersReducedMotion ? 1.015 : 1,\n        y: isDragging && !disabled && !prefersReducedMotion ? -2 : 0,\n      }}\n      aria-describedby={ariaDescribedBy || undefined}\n      aria-disabled={disabled || atMaxFiles || undefined}\n      aria-invalid={showError || undefined}\n      aria-label={ariaLabel}\n      className={[\n        \"relative overflow-hidden border bg-paper px-5 py-4\",\n        controlCornerClassName,\n        showError ? \"border-destructive\" : \"border-border\",\n        disabled || atMaxFiles\n          ? \"cursor-not-allowed opacity-60\"\n          : \"cursor-pointer\",\n      ].join(\" \")}\n      onClick={atMaxFiles ? undefined : onBrowse}\n      onDragEnter={onDragEnter}\n      onDragLeave={onDragLeave}\n      onDragOver={(event) => {\n        event.preventDefault();\n        if (!(disabled || atMaxFiles)) event.dataTransfer.dropEffect = \"copy\";\n      }}\n      onDrop={onDrop}\n      onKeyDown={onKeyDown}\n      role=\"button\"\n      style={{ boxShadow: \"var(--file-upload-shell-shadow)\" }}\n      tabIndex={disabled || atMaxFiles ? -1 : 0}\n      transition={shellTransition}\n    >\n      <input\n        accept={accept}\n        aria-hidden\n        className=\"hidden\"\n        disabled={disabled}\n        id={rootId}\n        multiple={multiple}\n        name={name}\n        onChange={onInputChange}\n        ref={inputRef}\n        required={required && filesCount === 0}\n        tabIndex={-1}\n        type=\"file\"\n      />\n\n      <div className=\"pointer-events-none relative flex items-center gap-4\">\n        <FileUploadDropzoneIcon\n          isDragging={isDragging}\n          prefersReducedMotion={prefersReducedMotion}\n          shellTransition={shellTransition}\n        />\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"font-medium text-foreground text-sm\">{title}</p>\n          <p className=\"truncate text-muted-foreground text-xs\" id={hintId}>\n            {dropzoneDescription}\n          </p>\n        </div>\n        <FileUploadBrowseHint\n          atMaxFiles={atMaxFiles}\n          browseLabel={browseLabel}\n          disabled={disabled}\n          prefersReducedMotion={prefersReducedMotion}\n        />\n      </div>\n    </motion.div>\n  );\n}\n\nfunction FileUploadDropzoneIcon({\n  isDragging,\n  prefersReducedMotion,\n  shellTransition,\n}: {\n  isDragging: boolean;\n  prefersReducedMotion: boolean;\n  shellTransition: Transition;\n}) {\n  return (\n    <div className=\"relative h-11 w-11 shrink-0\">\n      <motion.div\n        animate={{\n          rotate: isDragging && !prefersReducedMotion ? -14 : -8,\n          x: isDragging && !prefersReducedMotion ? -4 : -2,\n        }}\n        className={[\n          \"absolute inset-0 border border-border bg-card\",\n          controlCornerClassName,\n        ].join(\" \")}\n        transition={shellTransition}\n      />\n      <motion.div\n        animate={{\n          rotate: isDragging && !prefersReducedMotion ? 10 : 6,\n          x: isDragging && !prefersReducedMotion ? 4 : 2,\n        }}\n        className={[\n          \"absolute inset-0 border border-border bg-paper\",\n          controlCornerClassName,\n        ].join(\" \")}\n        transition={shellTransition}\n      />\n      <motion.div\n        animate={{ scale: isDragging && !prefersReducedMotion ? 1.06 : 1 }}\n        className={[\n          \"absolute inset-0 flex items-center justify-center bg-foreground text-background\",\n          controlCornerClassName,\n        ].join(\" \")}\n        style={{ boxShadow: \"var(--file-upload-icon-shadow)\" }}\n        transition={shellTransition}\n      >\n        <motion.div\n          animate={{ rotate: isDragging && !prefersReducedMotion ? 45 : 0 }}\n          transition={shellTransition}\n        >\n          <Plus className=\"h-4 w-4\" strokeWidth={2} />\n        </motion.div>\n      </motion.div>\n    </div>\n  );\n}\n\nfunction FileUploadBrowseHint({\n  atMaxFiles,\n  browseLabel,\n  disabled,\n  prefersReducedMotion,\n}: {\n  atMaxFiles: boolean;\n  browseLabel: string;\n  disabled: boolean;\n  prefersReducedMotion: boolean;\n}) {\n  if (prefersReducedMotion) {\n    return (\n      <div className=\"inline-flex shrink-0 items-center gap-1 text-[10px] text-foreground/70 uppercase tracking-[0.18em]\">\n        {browseLabel} <ArrowUpRight className=\"h-3 w-3\" />\n      </div>\n    );\n  }\n\n  return (\n    <motion.div\n      className=\"inline-flex shrink-0 items-center gap-1 text-[10px] text-foreground/70 uppercase tracking-[0.18em]\"\n      whileHover={disabled || atMaxFiles ? undefined : { x: 2, y: -1 }}\n    >\n      {browseLabel} <ArrowUpRight className=\"h-3 w-3\" />\n    </motion.div>\n  );\n}\n\nfunction FileUploadListHeader({\n  clearAll,\n  clearAllLabel,\n  disabled,\n  filesCount,\n  libraryLabel,\n  listLabelId,\n  prefersReducedMotion,\n  showClearAll,\n  totalDone,\n}: {\n  clearAll: () => void;\n  clearAllLabel: string;\n  disabled: boolean;\n  filesCount: number;\n  libraryLabel: string;\n  listLabelId: string;\n  prefersReducedMotion: boolean;\n  showClearAll: boolean;\n  totalDone: number;\n}) {\n  if (filesCount === 0) return null;\n\n  return (\n    <motion.div\n      animate={{ opacity: 1, y: 0 }}\n      className=\"mt-10 mb-4 flex items-end justify-between gap-3\"\n      exit={prefersReducedMotion ? undefined : { opacity: 0 }}\n      initial={prefersReducedMotion ? false : { opacity: 0, y: -6 }}\n    >\n      <div className=\"flex items-baseline gap-2\">\n        <span\n          className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\"\n          id={listLabelId}\n        >\n          {libraryLabel}\n        </span>\n        <span className=\"h-px w-10 bg-border\" />\n      </div>\n      <div className=\"flex items-center gap-3\">\n        {showClearAll ? (\n          <button\n            className=\"text-[11px] text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline disabled:pointer-events-none disabled:opacity-50\"\n            disabled={disabled}\n            onClick={(event) => {\n              event.stopPropagation();\n              clearAll();\n            }}\n            type=\"button\"\n          >\n            {clearAllLabel}\n          </button>\n        ) : null}\n        <div className=\"text-[11px] text-muted-foreground tabular-nums\">\n          <span className=\"font-medium text-foreground\">{totalDone}</span> of{\" \"}\n          {filesCount} ready\n        </div>\n      </div>\n    </motion.div>\n  );\n}\n\nexport const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(\n  function FileUpload(\n    {\n      accept,\n      ariaDescribedBy,\n      ariaLabel = \"Upload files\",\n      browseLabel = \"Browse\",\n      className,\n      clearAllLabel = \"Clear all\",\n      defaultValue,\n      description,\n      disabled = false,\n      dropzoneDescription,\n      dropzoneTitle,\n      id: idProp,\n      invalid = false,\n      libraryLabel = \"Library\",\n      maxFiles: maxFilesProp,\n      maxSize: maxSizeProp,\n      multiple = true,\n      name,\n      onFileRemove,\n      onFilesChange,\n      onReject,\n      onUpload,\n      onUploadComplete,\n      onValueChange,\n      preventDuplicates = false,\n      required = false,\n      showClearAll = true,\n      simulateUpload,\n      validateFile,\n      value,\n    },\n    ref\n  ) {\n    const generatedId = useId();\n    const rootId = idProp ?? generatedId;\n    const descriptionId = `${rootId}-description`;\n    const hintId = `${rootId}-hint`;\n    const errorId = `${rootId}-error`;\n    const liveId = `${rootId}-live`;\n    const listLabelId = `${rootId}-list-label`;\n\n    const maxFiles = resolveMaxFiles(maxFilesProp);\n    const maxSize = resolveMaxSize(maxSizeProp);\n    const prefersReducedMotion = useReducedMotion() === true;\n    const shouldSimulateUpload = simulateUpload ?? !onUpload;\n\n    const [internalFiles, setInternalFiles] = useState<FileUploadItem[]>(() =>\n      defaultValue?.length ? defaultValue.map(createUploadItem) : []\n    );\n    const [isDragging, setIsDragging] = useState(false);\n    const [inlineError, setInlineError] = useState<string | null>(null);\n    const [liveMessage, setLiveMessage] = useState(\"\");\n\n    const inputRef = useRef<HTMLInputElement>(null);\n    const rootRef = useRef<HTMLDivElement>(null);\n    const filesRef = useRef<FileUploadItem[]>([]);\n    const previousFilesRef = useRef<FileUploadItem[]>([]);\n    const completedSignatureRef = useRef(\"\");\n    const reportedSignatureRef = useRef(\"\");\n    const uploadRunRef = useRef<Set<string>>(new Set());\n    const uploadCancelledRef = useRef<Set<string>>(new Set());\n    const mountedRef = useRef(true);\n\n    const isControlled = value !== undefined;\n    const files = isControlled ? value : internalFiles;\n    const canSimulateUpload =\n      shouldSimulateUpload && !isControlled && !onUpload;\n    const atMaxFiles =\n      typeof maxFiles === \"number\" && maxFiles > 0 && files.length >= maxFiles;\n\n    const setFileItems = useCallback(\n      (\n        updater:\n          | FileUploadItem[]\n          | ((current: FileUploadItem[]) => FileUploadItem[])\n      ) => {\n        const resolveNext = (current: FileUploadItem[]) =>\n          typeof updater === \"function\" ? updater(current) : updater;\n\n        if (isControlled) {\n          const next = resolveNext(filesRef.current);\n          filesRef.current = next;\n          onValueChange?.(next);\n          return;\n        }\n\n        setInternalFiles((current) => {\n          const next = resolveNext(current);\n          filesRef.current = next;\n          onValueChange?.(next);\n          return next;\n        });\n      },\n      [isControlled, onValueChange]\n    );\n\n    const itemIsQueued = useCallback((id: string) => {\n      return filesRef.current.some((entry) => entry.id === id);\n    }, []);\n\n    const updateFileItem = useCallback(\n      (id: string, patch: Partial<FileUploadItem>) => {\n        if (uploadCancelledRef.current.has(id)) return;\n        if (!itemIsQueued(id)) return;\n\n        setFileItems((current) =>\n          current.map((item) => (item.id === id ? { ...item, ...patch } : item))\n        );\n      },\n      [itemIsQueued, setFileItems]\n    );\n\n    const announce = useCallback((message: string) => {\n      if (!mountedRef.current) return;\n      setLiveMessage(\"\");\n      requestAnimationFrame(() => {\n        if (mountedRef.current) setLiveMessage(message);\n      });\n    }, []);\n\n    const rejectFiles = useCallback(\n      (rejected: File[], reason: FileUploadRejectReason, message: string) => {\n        if (rejected.length === 0) return;\n        setInlineError(message);\n        onReject?.(rejected, reason, message);\n        announce(message);\n      },\n      [announce, onReject]\n    );\n\n    const runUpload = useCallback(\n      async (id: string) => {\n        if (!onUpload || uploadRunRef.current.has(id)) return;\n\n        const item = filesRef.current.find((entry) => entry.id === id);\n        if (!item) return;\n\n        await executeFileUpload({\n          announce,\n          file: item.file,\n          id,\n          itemIsQueued,\n          mountedRef,\n          onUpload,\n          updateFileItem,\n          uploadCancelledRef,\n          uploadRunRef,\n        });\n      },\n      [announce, itemIsQueued, onUpload, updateFileItem]\n    );\n\n    const queueUploads = useCallback(\n      (items: FileUploadItem[]) => {\n        if (!onUpload) return;\n        for (const item of items) {\n          if (item.status === \"uploading\") {\n            startUploadRun(runUpload, item.id);\n          }\n        }\n      },\n      [onUpload, runUpload]\n    );\n\n    const addFiles = useCallback(\n      (incoming: FileList | File[]) => {\n        const incomingList = Array.from(incoming);\n\n        if (disabled) {\n          rejectFiles(incomingList, \"disabled\", \"File upload is disabled.\");\n          return;\n        }\n\n        if (maxFiles === 0) {\n          rejectFiles(\n            incomingList,\n            \"max-files\",\n            \"File uploads are not allowed.\"\n          );\n          return;\n        }\n\n        const currentItems = filesRef.current;\n        const validation = validateIncomingFiles({\n          accept,\n          currentCount: currentItems.length,\n          files: incomingList,\n          maxFiles,\n          maxSize,\n          multiple,\n          preventDuplicates,\n          existingSignatures: new Set(\n            currentItems.map((item) => fileSignature(item.file))\n          ),\n          validateFile,\n        });\n\n        if (validation.accepted.length === 0) {\n          if (validation.rejected.length > 0) {\n            rejectFiles(\n              validation.rejected,\n              validation.reason,\n              validation.rejectionMessages[0] ?? \"No files were added.\"\n            );\n          }\n          return;\n        }\n\n        const { createdItems, keptIds, nextItems } = buildNextQueue({\n          acceptedFiles: validation.accepted,\n          currentItems,\n          maxFiles,\n          multiple,\n        });\n\n        const addedCount = Math.max(nextItems.length - currentItems.length, 0);\n        if (addedCount > 0) {\n          announce(\n            `${addedCount} file${addedCount === 1 ? \"\" : \"s\"} added to upload queue`\n          );\n        }\n\n        if (validation.rejected.length > 0) {\n          setInlineError(validation.rejectionMessages[0] ?? null);\n          onReject?.(\n            validation.rejected,\n            validation.reason,\n            validation.rejectionMessages[0] ?? \"Some files were rejected.\"\n          );\n        } else {\n          setInlineError(null);\n        }\n\n        filesRef.current = nextItems;\n        setFileItems(nextItems);\n        queueUploads(createdItems.filter((item) => keptIds.has(item.id)));\n      },\n      [\n        accept,\n        announce,\n        disabled,\n        maxFiles,\n        maxSize,\n        multiple,\n        onReject,\n        preventDuplicates,\n        queueUploads,\n        rejectFiles,\n        setFileItems,\n        validateFile,\n      ]\n    );\n\n    useEffect(() => {\n      mountedRef.current = true;\n      return () => {\n        mountedRef.current = false;\n      };\n    }, []);\n\n    useEffect(() => {\n      filesRef.current = files;\n    }, [files]);\n\n    useEffect(() => {\n      const previous = previousFilesRef.current;\n      const nextIds = new Set(files.map((item) => item.id));\n\n      for (const item of previous) {\n        if (!nextIds.has(item.id)) revokePreview(item);\n      }\n\n      previousFilesRef.current = files;\n    }, [files]);\n\n    useEffect(() => {\n      const input = inputRef.current;\n      if (!input) return;\n      syncInputFiles(input, files);\n    }, [files]);\n\n    useEffect(\n      () => () => {\n        revokeAllPreviews(filesRef.current);\n      },\n      []\n    );\n\n    useEffect(() => {\n      if (!canSimulateUpload) return;\n\n      const interval = window.setInterval(() => {\n        setFileItems((current) => simulateUploadTick(current));\n      }, 380);\n\n      return () => window.clearInterval(interval);\n    }, [canSimulateUpload, setFileItems]);\n\n    useEffect(() => {\n      queueUploads(filesRef.current);\n    }, [queueUploads]);\n\n    useEffect(() => {\n      const resetDragState = () => {\n        setIsDragging(false);\n      };\n\n      window.addEventListener(\"dragend\", resetDragState);\n\n      return () => {\n        window.removeEventListener(\"dragend\", resetDragState);\n      };\n    }, []);\n\n    const fileListSignature = files.map((item) => item.id).join(\"|\");\n\n    useEffect(() => {\n      if (!onFilesChange) return;\n      if (reportedSignatureRef.current === fileListSignature) return;\n\n      reportedSignatureRef.current = fileListSignature;\n      onFilesChange(files.map((item) => item.file));\n    }, [fileListSignature, files, onFilesChange]);\n\n    useEffect(() => {\n      if (!onUploadComplete) return;\n      if (files.length === 0) {\n        completedSignatureRef.current = \"\";\n        return;\n      }\n      if (files.some((item) => item.status !== \"done\")) return;\n      if (completedSignatureRef.current === fileListSignature) return;\n\n      completedSignatureRef.current = fileListSignature;\n      onUploadComplete(files.map((item) => item.file));\n      announce(\"All files are ready\");\n    }, [announce, fileListSignature, files, onUploadComplete]);\n\n    const removeFile = useCallback(\n      (id: string) => {\n        if (disabled) return;\n\n        const target = filesRef.current.find((item) => item.id === id);\n        if (!target) return;\n\n        uploadCancelledRef.current.add(id);\n        uploadRunRef.current.delete(id);\n        revokePreview(target);\n\n        const nextItems = filesRef.current.filter((item) => item.id !== id);\n        filesRef.current = nextItems;\n        setFileItems(nextItems);\n        onFileRemove?.(\n          target.file,\n          nextItems.map((item) => item.file)\n        );\n        announce(`${target.file.name} removed`);\n      },\n      [announce, disabled, onFileRemove, setFileItems]\n    );\n\n    const clearAll = useCallback(() => {\n      if (disabled || filesRef.current.length === 0) return;\n\n      for (const item of filesRef.current) {\n        uploadCancelledRef.current.add(item.id);\n      }\n      uploadRunRef.current.clear();\n      revokeAllPreviews(filesRef.current);\n      filesRef.current = [];\n      setFileItems([]);\n      setInlineError(null);\n      announce(\"All files cleared\");\n    }, [announce, disabled, setFileItems]);\n\n    const retryUpload = useCallback(\n      (id: string) => {\n        if (disabled) return;\n\n        uploadCancelledRef.current.delete(id);\n\n        if (onUpload) {\n          startUploadRun(runUpload, id);\n          return;\n        }\n\n        updateFileItem(id, {\n          status: \"uploading\",\n          progress: 0,\n          error: undefined,\n        });\n      },\n      [disabled, onUpload, runUpload, updateFileItem]\n    );\n\n    const handleDragEnter = (event: React.DragEvent) => {\n      event.preventDefault();\n      if (disabled || atMaxFiles) return;\n      setIsDragging(true);\n    };\n\n    const handleDragLeave = (event: React.DragEvent) => {\n      event.preventDefault();\n      if (disabled) return;\n\n      const related = event.relatedTarget as Node | null;\n      if (related && event.currentTarget.contains(related)) return;\n\n      setIsDragging(false);\n    };\n\n    const onDrop = (event: React.DragEvent) => {\n      event.preventDefault();\n      event.stopPropagation();\n      setIsDragging(false);\n      if (disabled || atMaxFiles) return;\n      if (event.dataTransfer.files?.length) addFiles(event.dataTransfer.files);\n    };\n\n    const handleBrowse = () => {\n      if (disabled) return;\n      inputRef.current?.click();\n    };\n\n    const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n      if (disabled || !event.target.files?.length) return;\n      addFiles(event.target.files);\n      event.target.value = \"\";\n    };\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (disabled) return;\n      if (event.key === \"Enter\" || event.key === \" \") {\n        event.preventDefault();\n        inputRef.current?.click();\n      }\n    };\n\n    useEffect(() => {\n      const root = rootRef.current;\n      if (!root) return;\n\n      const handlePaste = (event: ClipboardEvent) => {\n        if (disabled) return;\n        if (!root.contains(document.activeElement)) return;\n\n        const pastedFiles = Array.from(event.clipboardData?.files ?? []);\n        if (pastedFiles.length === 0) return;\n\n        event.preventDefault();\n        addFiles(pastedFiles);\n      };\n\n      root.addEventListener(\"paste\", handlePaste);\n      return () => root.removeEventListener(\"paste\", handlePaste);\n    }, [addFiles, disabled]);\n\n    const totalDone = files.filter((item) => item.status === \"done\").length;\n    const hasQueueError = files.some((item) => item.status === \"error\");\n    const showError = Boolean(inlineError) || invalid || hasQueueError;\n    const resolvedDropzoneTitle =\n      dropzoneTitle ??\n      (isDragging ? \"Release to upload\" : \"Drop, click, or paste to upload\");\n    const resolvedDropzoneDescription =\n      dropzoneDescription ?? buildDropzoneHint({ accept, maxFiles, maxSize });\n\n    const describedBy = [\n      description ? descriptionId : null,\n      hintId,\n      showError && inlineError ? errorId : null,\n      ariaDescribedBy,\n    ]\n      .filter(Boolean)\n      .join(\" \");\n\n    const shellTransition: Transition = prefersReducedMotion\n      ? { duration: 0 }\n      : { type: \"spring\", stiffness: 260, damping: 24 };\n\n    const listTransition: Transition = prefersReducedMotion\n      ? { duration: 0 }\n      : { type: \"spring\", stiffness: 320, damping: 30, mass: 0.6 };\n\n    return (\n      <div\n        className={[\n          componentThemeClassName,\n          \"[--file-upload-shell-shadow:0_12px_28px_-24px_rgba(15,23,42,0.18)]\",\n          \"[--file-upload-icon-shadow:0_10px_18px_-16px_rgba(15,23,42,0.16)]\",\n          \"dark:[--file-upload-shell-shadow:0_16px_32px_-26px_rgba(0,0,0,0.38)]\",\n          \"dark:[--file-upload-icon-shadow:0_12px_22px_-18px_rgba(0,0,0,0.32)]\",\n          \"mx-auto w-full max-w-2xl\",\n          className,\n        ]\n          .filter(Boolean)\n          .join(\" \")}\n        ref={(node) => {\n          rootRef.current = node;\n          setRef(ref, node);\n        }}\n      >\n        {description ? (\n          <p className=\"mb-2 text-muted-foreground text-sm\" id={descriptionId}>\n            {description}\n          </p>\n        ) : null}\n\n        <FileUploadDropzone\n          accept={accept}\n          ariaDescribedBy={describedBy}\n          ariaLabel={ariaLabel}\n          atMaxFiles={atMaxFiles}\n          browseLabel={browseLabel}\n          disabled={disabled}\n          dropzoneDescription={resolvedDropzoneDescription}\n          dropzoneTitle={resolvedDropzoneTitle}\n          filesCount={files.length}\n          hintId={hintId}\n          inputRef={inputRef}\n          isDragging={isDragging}\n          maxFiles={maxFiles}\n          multiple={multiple}\n          name={name}\n          onBrowse={handleBrowse}\n          onDragEnter={handleDragEnter}\n          onDragLeave={handleDragLeave}\n          onDrop={onDrop}\n          onInputChange={handleInputChange}\n          onKeyDown={handleKeyDown}\n          prefersReducedMotion={prefersReducedMotion}\n          required={required}\n          rootId={rootId}\n          shellTransition={shellTransition}\n          showError={showError}\n        />\n\n        {inlineError ? (\n          <p\n            className=\"mt-2 text-destructive text-xs\"\n            id={errorId}\n            role=\"alert\"\n          >\n            {inlineError}\n          </p>\n        ) : null}\n\n        <p aria-live=\"polite\" className=\"sr-only\" id={liveId}>\n          {liveMessage}\n        </p>\n\n        <AnimatePresence>\n          <FileUploadListHeader\n            clearAll={clearAll}\n            clearAllLabel={clearAllLabel}\n            disabled={disabled}\n            filesCount={files.length}\n            libraryLabel={libraryLabel}\n            listLabelId={listLabelId}\n            prefersReducedMotion={prefersReducedMotion}\n            showClearAll={showClearAll}\n            totalDone={totalDone}\n          />\n        </AnimatePresence>\n\n        <motion.ul\n          aria-labelledby={files.length > 0 ? listLabelId : undefined}\n          className=\"space-y-2.5\"\n          layout={!prefersReducedMotion}\n        >\n          <AnimatePresence initial={false}>\n            {files.map((item) => (\n              <FileUploadRow\n                disabled={disabled}\n                item={item}\n                key={item.id}\n                listTransition={listTransition}\n                onRemove={removeFile}\n                onRetry={retryUpload}\n                prefersReducedMotion={prefersReducedMotion}\n              />\n            ))}\n          </AnimatePresence>\n        </motion.ul>\n      </div>\n    );\n  }\n);\n\nFileUpload.displayName = \"FileUpload\";\n\nfunction FileUploadRowThumbnail({\n  item,\n  kind,\n  prefersReducedMotion,\n  progressValue,\n  showProgress,\n}: {\n  item: FileUploadItem;\n  kind: string;\n  prefersReducedMotion: boolean;\n  progressValue: number;\n  showProgress: boolean;\n}) {\n  if (item.preview) {\n    return (\n      <div\n        className={[\n          \"relative h-12 w-12 overflow-hidden border border-border\",\n          controlCornerClassName,\n        ].join(\" \")}\n      >\n        {item.file.type.startsWith(\"video/\") ? (\n          <video\n            aria-hidden\n            className=\"h-full w-full object-cover\"\n            muted\n            playsInline\n            preload=\"metadata\"\n            src={item.preview}\n          />\n        ) : (\n          /* biome-ignore lint/performance/noImgElement: registry component stays framework-agnostic for non-Next consumers. */\n          <img\n            alt=\"\"\n            aria-hidden\n            className=\"h-full w-full object-cover\"\n            height={48}\n            src={item.preview}\n            width={48}\n          />\n        )}\n        {showProgress ? (\n          <div\n            aria-hidden\n            className=\"absolute inset-0 flex items-center justify-center bg-background/55\"\n          >\n            <ProgressRing\n              label={`Upload progress for ${item.file.name}`}\n              progress={progressValue}\n              reduceMotion={prefersReducedMotion}\n              size={40}\n            />\n          </div>\n        ) : null}\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"relative flex h-12 w-12 items-center justify-center\">\n      <div className=\"absolute inset-0 flex items-center justify-center\">\n        <ProgressRing\n          label={`Upload progress for ${item.file.name}`}\n          progress={progressValue}\n          reduceMotion={prefersReducedMotion}\n        />\n      </div>\n      <div aria-hidden className=\"relative text-foreground/80\">\n        <KindIcon kind={kind} />\n      </div>\n    </div>\n  );\n}\n\nfunction FileUploadRowMeta({\n  item,\n  kind,\n  listTransition,\n  prefersReducedMotion,\n}: {\n  item: FileUploadItem;\n  kind: string;\n  listTransition: Transition;\n  prefersReducedMotion: boolean;\n}) {\n  return (\n    <div className=\"relative z-10 min-w-0 flex-1\">\n      <div className=\"flex items-center gap-2\">\n        <p className=\"truncate font-medium text-foreground text-sm\">\n          {item.file.name}\n        </p>\n        <AnimatePresence>\n          {item.status === \"done\" ? (\n            <motion.span\n              animate={\n                prefersReducedMotion ? undefined : { scale: 1, opacity: 1 }\n              }\n              aria-hidden\n              className=\"flex h-4 w-4 items-center justify-center rounded-full bg-foreground text-background\"\n              exit={\n                prefersReducedMotion ? undefined : { scale: 0.95, opacity: 0 }\n              }\n              initial={\n                prefersReducedMotion ? false : { scale: 0.95, opacity: 0 }\n              }\n              transition={listTransition}\n            >\n              <Check className=\"h-2.5 w-2.5\" strokeWidth={3} />\n            </motion.span>\n          ) : null}\n        </AnimatePresence>\n      </div>\n      <div className=\"mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground tabular-nums\">\n        <span className=\"uppercase tracking-wider\">{kind}</span>\n        <span\n          aria-hidden\n          className=\"h-0.5 w-0.5 rounded-full bg-muted-foreground/60\"\n        />\n        <span>{formatBytes(item.file.size)}</span>\n        {item.status === \"uploading\" ? (\n          <span aria-live=\"polite\" className=\"ml-auto text-foreground/80\">\n            {Math.round(item.progress)}%\n          </span>\n        ) : null}\n        {item.status === \"error\" && item.error ? (\n          <span className=\"ml-auto truncate text-destructive\" role=\"alert\">\n            {item.error}\n          </span>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nfunction FileUploadRowActions({\n  disabled,\n  item,\n  onRemove,\n  onRetry,\n}: {\n  disabled: boolean;\n  item: FileUploadItem;\n  onRemove: (id: string) => void;\n  onRetry: (id: string) => void;\n}) {\n  return (\n    <div className=\"relative z-10 flex items-center gap-1\">\n      {item.status === \"error\" ? (\n        <button\n          aria-label={`Retry upload for ${item.file.name}`}\n          className={[\n            \"flex h-8 w-8 items-center justify-center text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50\",\n            controlCornerClassName,\n          ].join(\" \")}\n          disabled={disabled}\n          onClick={(event) => {\n            event.stopPropagation();\n            onRetry(item.id);\n          }}\n          type=\"button\"\n        >\n          <RefreshCw className=\"h-3.5 w-3.5\" />\n        </button>\n      ) : null}\n      <button\n        aria-label={`Remove ${item.file.name}`}\n        className={[\n          \"flex h-8 w-8 items-center justify-center text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50\",\n          controlCornerClassName,\n        ].join(\" \")}\n        disabled={disabled}\n        onClick={(event) => {\n          event.stopPropagation();\n          onRemove(item.id);\n        }}\n        type=\"button\"\n      >\n        <X className=\"h-3.5 w-3.5\" />\n      </button>\n    </div>\n  );\n}\n\nfunction FileUploadRow({\n  disabled,\n  item,\n  listTransition,\n  onRemove,\n  onRetry,\n  prefersReducedMotion,\n}: {\n  disabled: boolean;\n  item: FileUploadItem;\n  listTransition: Transition;\n  onRemove: (id: string) => void;\n  onRetry: (id: string) => void;\n  prefersReducedMotion: boolean;\n}) {\n  const kind = kindOf(item.file);\n  const showProgress = item.status === \"uploading\";\n  const progressValue = item.status === \"done\" ? 100 : item.progress;\n\n  return (\n    <motion.li\n      animate={\n        prefersReducedMotion\n          ? undefined\n          : { opacity: 1, y: 0, scale: 1, filter: \"blur(0px)\" }\n      }\n      className={[\n        \"group relative flex items-center gap-4 overflow-hidden border border-border bg-card p-3 pr-4\",\n        controlCornerClassName,\n        item.status === \"error\" ? \"border-destructive/40\" : \"\",\n      ].join(\" \")}\n      exit={\n        prefersReducedMotion\n          ? undefined\n          : { opacity: 0, x: 60, scale: 0.92, filter: \"blur(4px)\" }\n      }\n      initial={\n        prefersReducedMotion\n          ? false\n          : { opacity: 0, y: 16, scale: 0.97, filter: \"blur(6px)\" }\n      }\n      layout={!prefersReducedMotion}\n      transition={listTransition}\n    >\n      <div className=\"relative z-10 flex h-12 w-12 shrink-0 items-center justify-center\">\n        <FileUploadRowThumbnail\n          item={item}\n          kind={kind}\n          prefersReducedMotion={prefersReducedMotion}\n          progressValue={progressValue}\n          showProgress={showProgress}\n        />\n      </div>\n\n      <FileUploadRowMeta\n        item={item}\n        kind={kind}\n        listTransition={listTransition}\n        prefersReducedMotion={prefersReducedMotion}\n      />\n\n      <FileUploadRowActions\n        disabled={disabled}\n        item={item}\n        onRemove={onRemove}\n        onRetry={onRetry}\n      />\n    </motion.li>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "title": "File Upload",
  "description": "Drag-and-drop file uploader with click-to-browse fallback, queued file rows, image previews, built-in progress states, and optional change callbacks."
}
