{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "comp-553",
  "type": "registry:component",
  "registryDependencies": [
    "https://coss.com/origin/r/button.json"
  ],
  "files": [
    {
      "path": "registry/default/components/comp-553.tsx",
      "content": "\"use client\";\n\nimport {\n  AlertCircleIcon,\n  FileArchiveIcon,\n  FileIcon,\n  FileSpreadsheetIcon,\n  FileTextIcon,\n  HeadphonesIcon,\n  ImageIcon,\n  Trash2Icon,\n  UploadIcon,\n  VideoIcon,\n  XIcon,\n} from \"lucide-react\";\nimport { useState } from \"react\";\n\nimport {\n  type FileWithPreview,\n  formatBytes,\n  useFileUpload,\n} from \"@/registry/default/hooks/use-file-upload\";\nimport { Button } from \"@/registry/default/ui/button\";\n\n// Create some dummy initial files\nconst initialFiles = [\n  {\n    id: \"intro.zip-1744638436563-8u5xuls\",\n    name: \"intro.zip\",\n    size: 252873,\n    type: \"application/zip\",\n    url: \"https://example.com/intro.zip\",\n  },\n  {\n    id: \"image-01-123456789\",\n    name: \"image-01.jpg\",\n    size: 1528737,\n    type: \"image/jpeg\",\n    url: \"https://picsum.photos/1000/800?grayscale&random=1\",\n  },\n  {\n    id: \"audio-123456789\",\n    name: \"audio.mp3\",\n    size: 1528737,\n    type: \"audio/mpeg\",\n    url: \"https://example.com/audio.mp3\",\n  },\n];\n\nconst getFileIcon = (file: { file: File | { type: string; name: string } }) => {\n  const fileType = file.file instanceof File ? file.file.type : file.file.type;\n  const fileName = file.file instanceof File ? file.file.name : file.file.name;\n\n  const iconMap = {\n    archive: {\n      conditions: (type: string, name: string) =>\n        type.includes(\"zip\") ||\n        type.includes(\"archive\") ||\n        name.endsWith(\".zip\") ||\n        name.endsWith(\".rar\"),\n      icon: FileArchiveIcon,\n    },\n    audio: {\n      conditions: (type: string) => type.includes(\"audio/\"),\n      icon: HeadphonesIcon,\n    },\n    excel: {\n      conditions: (type: string, name: string) =>\n        type.includes(\"excel\") ||\n        name.endsWith(\".xls\") ||\n        name.endsWith(\".xlsx\"),\n      icon: FileSpreadsheetIcon,\n    },\n    image: {\n      conditions: (type: string) => type.startsWith(\"image/\"),\n      icon: ImageIcon,\n    },\n    pdf: {\n      conditions: (type: string, name: string) =>\n        type.includes(\"pdf\") ||\n        name.endsWith(\".pdf\") ||\n        type.includes(\"word\") ||\n        name.endsWith(\".doc\") ||\n        name.endsWith(\".docx\"),\n      icon: FileTextIcon,\n    },\n    video: {\n      conditions: (type: string) => type.includes(\"video/\"),\n      icon: VideoIcon,\n    },\n  };\n\n  for (const { icon: Icon, conditions } of Object.values(iconMap)) {\n    if (conditions(fileType, fileName)) {\n      return <Icon className=\"size-5 opacity-60\" />;\n    }\n  }\n\n  return <FileIcon className=\"size-5 opacity-60\" />;\n};\n\nconst _getFilePreview = (file: {\n  file: File | { type: string; name: string; url?: string };\n}) => {\n  const fileType = file.file instanceof File ? file.file.type : file.file.type;\n  const fileName = file.file instanceof File ? file.file.name : file.file.name;\n\n  const renderImage = (src: string) => (\n    <img\n      alt={fileName}\n      className=\"size-full rounded-t-[inherit] object-cover\"\n      src={src}\n    />\n  );\n\n  return (\n    <div className=\"flex aspect-square items-center justify-center overflow-hidden rounded-t-[inherit] bg-accent\">\n      {fileType.startsWith(\"image/\") ? (\n        file.file instanceof File ? (\n          (() => {\n            const previewUrl = URL.createObjectURL(file.file);\n            return renderImage(previewUrl);\n          })()\n        ) : file.file.url ? (\n          renderImage(file.file.url)\n        ) : (\n          <ImageIcon className=\"size-5 opacity-60\" />\n        )\n      ) : (\n        getFileIcon(file)\n      )}\n    </div>\n  );\n};\n\n// Type for tracking upload progress\ntype UploadProgress = {\n  fileId: string;\n  progress: number;\n  completed: boolean;\n};\n\n// Function to simulate file upload with more realistic timing and progress\nconst simulateUpload = (\n  totalBytes: number,\n  onProgress: (progress: number) => void,\n  onComplete: () => void,\n) => {\n  let timeoutId: NodeJS.Timeout;\n  let uploadedBytes = 0;\n  let lastProgressReport = 0;\n\n  const simulateChunk = () => {\n    // Simulate variable network conditions with random chunk sizes\n    const chunkSize = Math.floor(Math.random() * 300000) + 2000;\n    uploadedBytes = Math.min(totalBytes, uploadedBytes + chunkSize);\n\n    // Calculate progress percentage (0-100)\n    const progressPercent = Math.floor((uploadedBytes / totalBytes) * 100);\n\n    // Only report progress if it's changed by at least 1%\n    if (progressPercent > lastProgressReport) {\n      lastProgressReport = progressPercent;\n      onProgress(progressPercent);\n    }\n\n    // Continue simulation if not complete\n    if (uploadedBytes < totalBytes) {\n      // Variable delay between 50ms and 500ms to simulate network fluctuations (reduced for faster uploads)\n      const delay = Math.floor(Math.random() * 450) + 50;\n\n      // Occasionally add a longer pause to simulate network congestion (5% chance, shorter duration)\n      const extraDelay = Math.random() < 0.05 ? 500 : 0;\n\n      timeoutId = setTimeout(simulateChunk, delay + extraDelay);\n    } else {\n      // Upload complete\n      onComplete();\n    }\n  };\n\n  // Start the simulation\n  timeoutId = setTimeout(simulateChunk, 100);\n\n  // Return a cleanup function to cancel the simulation\n  return () => {\n    if (timeoutId) {\n      clearTimeout(timeoutId);\n    }\n  };\n};\n\nexport default function Component() {\n  const maxSizeMB = 5;\n  const maxSize = maxSizeMB * 1024 * 1024; // 5MB default\n  const maxFiles = 6;\n\n  // State to track upload progress for each file\n  const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);\n\n  // Function to handle newly added files\n  const handleFilesAdded = (addedFiles: FileWithPreview[]) => {\n    const newProgressItems = addedFiles.map((file) => ({\n      completed: false,\n      fileId: file.id,\n      progress: 0,\n    }));\n\n    setUploadProgress((prev) => [...prev, ...newProgressItems]);\n\n    const cleanupFunctions: Array<() => void> = [];\n\n    for (const file of addedFiles) {\n      const fileSize =\n        file.file instanceof File ? file.file.size : file.file.size;\n\n      const cleanup = simulateUpload(\n        fileSize,\n        (progress) => {\n          setUploadProgress((prev) =>\n            prev.map((item) =>\n              item.fileId === file.id ? { ...item, progress } : item,\n            ),\n          );\n        },\n        () => {\n          setUploadProgress((prev) =>\n            prev.map((item) =>\n              item.fileId === file.id ? { ...item, completed: true } : item,\n            ),\n          );\n        },\n      );\n\n      cleanupFunctions.push(cleanup);\n    }\n\n    return () => {\n      for (const cleanup of cleanupFunctions) {\n        cleanup();\n      }\n    };\n  };\n\n  // Remove the progress tracking for the file\n  const handleFileRemoved = (fileId: string) => {\n    setUploadProgress((prev) => prev.filter((item) => item.fileId !== fileId));\n  };\n\n  const [\n    { files, isDragging, errors },\n    {\n      handleDragEnter,\n      handleDragLeave,\n      handleDragOver,\n      handleDrop,\n      openFileDialog,\n      removeFile,\n      clearFiles,\n      getInputProps,\n    },\n  ] = useFileUpload({\n    initialFiles,\n    maxFiles,\n    maxSize,\n    multiple: true,\n    onFilesAdded: handleFilesAdded,\n  });\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      {/* Drop area */}\n      <div\n        className=\"relative flex min-h-52 flex-col items-center not-data-[files]:justify-center overflow-hidden rounded-xl border border-input border-dashed p-4 transition-colors has-[input:focus]:border-ring has-[input:focus]:ring-[3px] has-[input:focus]:ring-ring/50 data-[dragging=true]:bg-accent/50\"\n        data-dragging={isDragging || undefined}\n        data-files={files.length > 0 || undefined}\n        onDragEnter={handleDragEnter}\n        onDragLeave={handleDragLeave}\n        onDragOver={handleDragOver}\n        onDrop={handleDrop}\n      >\n        <input\n          {...getInputProps()}\n          aria-label=\"Upload image file\"\n          className=\"sr-only\"\n        />\n        {files.length > 0 ? (\n          <div className=\"flex w-full flex-col gap-3\">\n            <div className=\"flex items-center justify-between gap-2\">\n              <h3 className=\"truncate font-medium text-sm\">\n                Files ({files.length})\n              </h3>\n              <div className=\"flex gap-2\">\n                <Button onClick={openFileDialog} size=\"sm\" variant=\"outline\">\n                  <UploadIcon\n                    aria-hidden=\"true\"\n                    className=\"-ms-0.5 size-3.5 opacity-60\"\n                  />\n                  Add files\n                </Button>\n                <Button\n                  onClick={() => {\n                    // Clear all progress tracking\n                    setUploadProgress([]);\n                    clearFiles();\n                  }}\n                  size=\"sm\"\n                  variant=\"outline\"\n                >\n                  <Trash2Icon\n                    aria-hidden=\"true\"\n                    className=\"-ms-0.5 size-3.5 opacity-60\"\n                  />\n                  Remove all\n                </Button>\n              </div>\n            </div>\n\n            <div className=\"w-full space-y-2\">\n              {files.map((file) => {\n                // Find the upload progress for this file once to avoid repeated lookups\n                const fileProgress = uploadProgress.find(\n                  (p) => p.fileId === file.id,\n                );\n                const isUploading = fileProgress && !fileProgress.completed;\n\n                return (\n                  <div\n                    className=\"flex flex-col gap-1 rounded-lg border bg-background p-2 pe-3 transition-opacity duration-300\"\n                    data-uploading={isUploading || undefined}\n                    key={file.id}\n                  >\n                    <div className=\"flex items-center justify-between gap-2\">\n                      <div className=\"flex items-center gap-3 overflow-hidden in-data-[uploading=true]:opacity-50\">\n                        <div className=\"flex aspect-square size-10 shrink-0 items-center justify-center rounded border\">\n                          {getFileIcon(file)}\n                        </div>\n                        <div className=\"flex min-w-0 flex-col gap-0.5\">\n                          <p className=\"truncate font-medium text-[13px]\">\n                            {file.file instanceof File\n                              ? file.file.name\n                              : file.file.name}\n                          </p>\n                          <p className=\"text-muted-foreground text-xs\">\n                            {formatBytes(\n                              file.file instanceof File\n                                ? file.file.size\n                                : file.file.size,\n                            )}\n                          </p>\n                        </div>\n                      </div>\n                      <Button\n                        aria-label=\"Remove file\"\n                        className=\"-me-2 size-8 text-muted-foreground/80 hover:bg-transparent hover:text-foreground\"\n                        onClick={() => {\n                          handleFileRemoved(file.id);\n                          removeFile(file.id);\n                        }}\n                        size=\"icon\"\n                        variant=\"ghost\"\n                      >\n                        <XIcon aria-hidden=\"true\" className=\"size-4\" />\n                      </Button>\n                    </div>\n\n                    {/* Upload progress bar */}\n                    {fileProgress &&\n                      (() => {\n                        const progress = fileProgress.progress || 0;\n                        const completed = fileProgress.completed || false;\n\n                        if (completed) return null;\n\n                        return (\n                          <div className=\"mt-1 flex items-center gap-2\">\n                            <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-gray-100\">\n                              <div\n                                className=\"h-full bg-primary transition-all duration-300 ease-out\"\n                                style={{ width: `${progress}%` }}\n                              />\n                            </div>\n                            <span className=\"w-10 text-muted-foreground text-xs tabular-nums\">\n                              {progress}%\n                            </span>\n                          </div>\n                        );\n                      })()}\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n        ) : (\n          <div className=\"flex flex-col items-center justify-center px-4 py-3 text-center\">\n            <div\n              aria-hidden=\"true\"\n              className=\"mb-2 flex size-11 shrink-0 items-center justify-center rounded-full border bg-background\"\n            >\n              <ImageIcon className=\"size-4 opacity-60\" />\n            </div>\n            <p className=\"mb-1.5 font-medium text-sm\">Drop your files here</p>\n            <p className=\"text-muted-foreground text-xs\">\n              Max {maxFiles} files ∙ Up to {maxSizeMB}MB\n            </p>\n            <Button className=\"mt-4\" onClick={openFileDialog} variant=\"outline\">\n              <UploadIcon aria-hidden=\"true\" className=\"-ms-1 opacity-60\" />\n              Select images\n            </Button>\n          </div>\n        )}\n      </div>\n\n      {errors.length > 0 && (\n        <div\n          className=\"flex items-center gap-1 text-destructive text-xs\"\n          role=\"alert\"\n        >\n          <AlertCircleIcon className=\"size-3 shrink-0\" />\n          <span>{errors[0]}</span>\n        </div>\n      )}\n\n      <p\n        aria-live=\"polite\"\n        className=\"mt-2 text-center text-muted-foreground text-xs\"\n        role=\"region\"\n      >\n        With simulated progress track ∙{\" \"}\n        <a\n          className=\"underline hover:text-foreground\"\n          href=\"https://github.com/cosscom/coss/blob/main/apps/origin/docs/use-file-upload.md\"\n          rel=\"noreferrer\"\n          target=\"_blank\"\n        >\n          API\n        </a>\n      </p>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/hooks/use-file-upload.ts",
      "content": "\"use client\";\n\nimport {\n  type ChangeEvent,\n  type DragEvent,\n  type InputHTMLAttributes,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\n\nexport type FileMetadata = {\n  name: string;\n  size: number;\n  type: string;\n  url: string;\n  id: string;\n};\n\nexport type FileWithPreview = {\n  file: File | FileMetadata;\n  id: string;\n  preview?: string;\n};\n\nexport type FileUploadOptions = {\n  maxFiles?: number; // Only used when multiple is true, defaults to Infinity\n  maxSize?: number; // in bytes\n  accept?: string;\n  multiple?: boolean; // Defaults to false\n  initialFiles?: FileMetadata[];\n  onFilesChange?: (files: FileWithPreview[]) => void; // Callback when files change\n  onFilesAdded?: (addedFiles: FileWithPreview[]) => void; // Callback when new files are added\n};\n\nexport type FileUploadState = {\n  files: FileWithPreview[];\n  isDragging: boolean;\n  errors: string[];\n};\n\nexport type FileUploadActions = {\n  addFiles: (files: FileList | File[]) => void;\n  removeFile: (id: string) => void;\n  clearFiles: () => void;\n  clearErrors: () => void;\n  handleDragEnter: (e: DragEvent<HTMLElement>) => void;\n  handleDragLeave: (e: DragEvent<HTMLElement>) => void;\n  handleDragOver: (e: DragEvent<HTMLElement>) => void;\n  handleDrop: (e: DragEvent<HTMLElement>) => void;\n  handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void;\n  openFileDialog: () => void;\n  getInputProps: (\n    props?: InputHTMLAttributes<HTMLInputElement>,\n  ) => InputHTMLAttributes<HTMLInputElement> & {\n    // Use `any` here to avoid cross-React ref type conflicts across packages\n    // biome-ignore lint/suspicious/noExplicitAny: intentional\n    ref: any;\n  };\n};\n\nexport const useFileUpload = (\n  options: FileUploadOptions = {},\n): [FileUploadState, FileUploadActions] => {\n  const {\n    maxFiles = Number.POSITIVE_INFINITY,\n    maxSize = Number.POSITIVE_INFINITY,\n    accept = \"*\",\n    multiple = false,\n    initialFiles = [],\n    onFilesChange,\n    onFilesAdded,\n  } = options;\n\n  const [state, setState] = useState<FileUploadState>({\n    errors: [],\n    files: initialFiles.map((file) => ({\n      file,\n      id: file.id,\n      preview: file.url,\n    })),\n    isDragging: false,\n  });\n\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const validateFile = useCallback(\n    (file: File | FileMetadata): string | null => {\n      if (file instanceof File) {\n        if (file.size > maxSize) {\n          return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`;\n        }\n      } else {\n        if (file.size > maxSize) {\n          return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`;\n        }\n      }\n\n      if (accept !== \"*\") {\n        const acceptedTypes = accept.split(\",\").map((type) => type.trim());\n        const fileType = file instanceof File ? file.type || \"\" : file.type;\n        const fileExtension = `.${file instanceof File ? file.name.split(\".\").pop() : file.name.split(\".\").pop()}`;\n\n        const isAccepted = acceptedTypes.some((type) => {\n          if (type.startsWith(\".\")) {\n            return fileExtension.toLowerCase() === type.toLowerCase();\n          }\n          if (type.endsWith(\"/*\")) {\n            const baseType = type.split(\"/\")[0];\n            return fileType.startsWith(`${baseType}/`);\n          }\n          return fileType === type;\n        });\n\n        if (!isAccepted) {\n          return `File \"${file instanceof File ? file.name : file.name}\" is not an accepted file type.`;\n        }\n      }\n\n      return null;\n    },\n    [accept, maxSize],\n  );\n\n  const createPreview = useCallback(\n    (file: File | FileMetadata): string | undefined => {\n      if (file instanceof File) {\n        return URL.createObjectURL(file);\n      }\n      return file.url;\n    },\n    [],\n  );\n\n  const generateUniqueId = useCallback((file: File | FileMetadata): string => {\n    if (file instanceof File) {\n      return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;\n    }\n    return file.id;\n  }, []);\n\n  const clearFiles = useCallback(() => {\n    setState((prev) => {\n      // Clean up object URLs\n      for (const file of prev.files ?? []) {\n        if (\n          file.preview &&\n          file.file instanceof File &&\n          file.file.type.startsWith(\"image/\")\n        ) {\n          URL.revokeObjectURL(file.preview);\n        }\n      }\n\n      if (inputRef.current) {\n        inputRef.current.value = \"\";\n      }\n\n      const newState = {\n        ...prev,\n        errors: [],\n        files: [],\n      };\n\n      onFilesChange?.(newState.files);\n      return newState;\n    });\n  }, [onFilesChange]);\n\n  const addFiles = useCallback(\n    (newFiles: FileList | File[]) => {\n      if (!newFiles || newFiles.length === 0) return;\n\n      const newFilesArray = Array.from(newFiles);\n      const errors: string[] = [];\n\n      // Clear existing errors when new files are uploaded\n      setState((prev) => ({ ...prev, errors: [] }));\n\n      // In single file mode, clear existing files first\n      if (!multiple) {\n        clearFiles();\n      }\n\n      // Check if adding these files would exceed maxFiles (only in multiple mode)\n      if (\n        multiple &&\n        maxFiles !== Number.POSITIVE_INFINITY &&\n        state.files.length + newFilesArray.length > maxFiles\n      ) {\n        errors.push(`You can only upload a maximum of ${maxFiles} files.`);\n        setState((prev) => ({ ...prev, errors }));\n        return;\n      }\n\n      const validFiles: FileWithPreview[] = [];\n\n      for (const file of newFilesArray) {\n        if (multiple) {\n          const isDuplicate = state.files.some(\n            (existingFile) =>\n              existingFile.file.name === file.name &&\n              existingFile.file.size === file.size,\n          );\n\n          if (isDuplicate) {\n            continue;\n          }\n        }\n\n        if (file.size > maxSize) {\n          errors.push(\n            multiple\n              ? `Some files exceed the maximum size of ${formatBytes(maxSize)}.`\n              : `File exceeds the maximum size of ${formatBytes(maxSize)}.`,\n          );\n          continue;\n        }\n\n        const error = validateFile(file);\n\n        if (error) {\n          errors.push(error);\n          continue;\n        }\n\n        validFiles.push({\n          file,\n          id: generateUniqueId(file),\n          preview: createPreview(file),\n        });\n      }\n\n      // Only update state if we have valid files to add\n      if (validFiles.length > 0) {\n        // Call the onFilesAdded callback with the newly added valid files\n        onFilesAdded?.(validFiles);\n\n        setState((prev) => {\n          const newFiles = !multiple\n            ? validFiles\n            : [...prev.files, ...validFiles];\n          onFilesChange?.(newFiles);\n          return {\n            ...prev,\n            errors,\n            files: newFiles,\n          };\n        });\n      } else if (errors.length > 0) {\n        setState((prev) => ({\n          ...prev,\n          errors,\n        }));\n      }\n\n      // Reset input value after handling files\n      if (inputRef.current) {\n        inputRef.current.value = \"\";\n      }\n    },\n    [\n      state.files,\n      maxFiles,\n      multiple,\n      maxSize,\n      validateFile,\n      createPreview,\n      generateUniqueId,\n      clearFiles,\n      onFilesChange,\n      onFilesAdded,\n    ],\n  );\n\n  const removeFile = useCallback(\n    (id: string) => {\n      setState((prev) => {\n        const fileToRemove = prev.files.find((file) => file.id === id);\n        if (\n          fileToRemove?.preview &&\n          fileToRemove.file instanceof File &&\n          fileToRemove.file.type.startsWith(\"image/\")\n        ) {\n          URL.revokeObjectURL(fileToRemove.preview);\n        }\n\n        const newFiles = prev.files.filter((file) => file.id !== id);\n        onFilesChange?.(newFiles);\n\n        return {\n          ...prev,\n          errors: [],\n          files: newFiles,\n        };\n      });\n    },\n    [onFilesChange],\n  );\n\n  const clearErrors = useCallback(() => {\n    setState((prev) => ({\n      ...prev,\n      errors: [],\n    }));\n  }, []);\n\n  const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setState((prev) => ({ ...prev, isDragging: true }));\n  }, []);\n\n  const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    if (e.currentTarget.contains(e.relatedTarget as Node)) {\n      return;\n    }\n\n    setState((prev) => ({ ...prev, isDragging: false }));\n  }, []);\n\n  const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n  }, []);\n\n  const handleDrop = useCallback(\n    (e: DragEvent<HTMLElement>) => {\n      e.preventDefault();\n      e.stopPropagation();\n      setState((prev) => ({ ...prev, isDragging: false }));\n\n      // Don't process files if the input is disabled\n      if (inputRef.current?.disabled) {\n        return;\n      }\n\n      if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n        // In single file mode, only use the first file\n        if (!multiple) {\n          const file = e.dataTransfer.files[0];\n          addFiles([file]);\n        } else {\n          addFiles(e.dataTransfer.files);\n        }\n      }\n    },\n    [addFiles, multiple],\n  );\n\n  const handleFileChange = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      if (e.target.files && e.target.files.length > 0) {\n        addFiles(e.target.files);\n      }\n    },\n    [addFiles],\n  );\n\n  const openFileDialog = useCallback(() => {\n    if (inputRef.current) {\n      inputRef.current.click();\n    }\n  }, []);\n\n  const getInputProps = useCallback(\n    (props: InputHTMLAttributes<HTMLInputElement> = {}) => {\n      return {\n        ...props,\n        accept: props.accept || accept,\n        multiple: props.multiple !== undefined ? props.multiple : multiple,\n        onChange: handleFileChange,\n        // Cast to `any` to prevent mismatched React ref type errors across workspaces\n        // biome-ignore lint/suspicious/noExplicitAny: Intentional\n        ref: inputRef as any,\n        type: \"file\" as const,\n      };\n    },\n    [accept, multiple, handleFileChange],\n  );\n\n  return [\n    state,\n    {\n      addFiles,\n      clearErrors,\n      clearFiles,\n      getInputProps,\n      handleDragEnter,\n      handleDragLeave,\n      handleDragOver,\n      handleDrop,\n      handleFileChange,\n      openFileDialog,\n      removeFile,\n    },\n  ];\n};\n\n// Helper function to format bytes to human-readable format\nexport const formatBytes = (bytes: number, decimals = 2): string => {\n  if (bytes === 0) return \"0 Bytes\";\n\n  const k = 1024;\n  const dm = decimals < 0 ? 0 : decimals;\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"];\n\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n  return Number.parseFloat((bytes / k ** i).toFixed(dm)) + sizes[i];\n};\n",
      "type": "registry:hook"
    }
  ],
  "meta": {
    "colSpan": 2,
    "tags": [
      "upload",
      "file",
      "image",
      "drag and drop"
    ]
  }
}