{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload",
  "title": "File Upload",
  "description": "A drag-and-drop file upload surface with accepted file type icons and a compact file list.",
  "dependencies": [
    "border-beam@^1.0.1"
  ],
  "registryDependencies": [
    "@extend/file-thumbnail",
    "utils",
    "card"
  ],
  "files": [
    {
      "path": "components/extend/file-upload.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { BorderBeam } from \"border-beam\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Card } from \"@/components/ui/card\"\nimport { FileThumbnail } from \"@/components/extend/file-thumbnail\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\nfunction FileImageGlyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"FileImage\"\n      tabler=\"IconPhoto\"\n      hugeicons=\"FileImageIcon\"\n      phosphor=\"FileImageIcon\"\n      remixicon=\"RiFileImageLine\"\n      {...props}\n    />\n  )\n}\nfunction FileSpreadsheetGlyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"FileSpreadsheet\"\n      tabler=\"IconFileSpreadsheet\"\n      hugeicons=\"FileSpreadsheetIcon\"\n      phosphor=\"FileXlsIcon\"\n      remixicon=\"RiFileExcel2Line\"\n      {...props}\n    />\n  )\n}\nfunction FileUploadGlyph(props: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"FileUp\"\n      tabler=\"IconFileUpload\"\n      hugeicons=\"FileUploadIcon\"\n      phosphor=\"FileArrowUpIcon\"\n      remixicon=\"RiFileUploadLine\"\n      {...props}\n    />\n  )\n}\ntype FileUploadItem = {\n  id: string\n  name: string\n  type: string\n  size: number\n  url: string\n}\ntype AcceptedFileType = {\n  label: string\n  icon: React.ComponentType<InlineRegistryIconProps>\n}\ntype FileUploadProps = {\n  accept?: string\n  acceptedFileTypes?: AcceptedFileType[]\n  borderBeamTheme?: React.ComponentProps<typeof BorderBeam>[\"theme\"]\n  browseLabel?: string\n  className?: string\n  description?: string\n  draggingLabel?: string\n  multiple?: boolean\n  showBorderBeam?: boolean\n  showFileList?: boolean\n  title?: string\n  onFilesAccepted?: (files: File[]) => void\n  onFilesChange?: (files: FileUploadItem[]) => void\n}\nconst ACCEPTED_FILE_TYPES: AcceptedFileType[] = [\n  { label: \"Image\", icon: FileImageGlyph },\n  { label: \"PDF\", icon: FileUploadGlyph },\n  { label: \"Sheet\", icon: FileSpreadsheetGlyph },\n]\nconst DEFAULT_ACCEPT = [\n  \".pdf\",\n  \".doc\",\n  \".docx\",\n  \".xlsx\",\n  \".csv\",\n  \".png\",\n  \".jpg\",\n  \".jpeg\",\n  \"application/pdf\",\n  \"application/msword\",\n  \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n  \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n  \"text/csv\",\n  \"image/png\",\n  \"image/jpeg\",\n].join(\",\")\nconst ICON_TRANSFORMS = [\n  {\n    idle: \"translate(-78%, -50%) rotate(-8deg)\",\n    active: \"translate(-114%, -50%) rotate(-12deg) scale(1.08)\",\n  },\n  {\n    idle: \"translate(-50%, -50%) rotate(0deg)\",\n    active: \"translate(-50%, -50%) rotate(0deg) scale(1.18)\",\n  },\n  {\n    idle: \"translate(-22%, -50%) rotate(8deg)\",\n    active: \"translate(14%, -50%) rotate(12deg) scale(1.08)\",\n  },\n]\nfunction formatBytes(bytes: number) {\n  if (bytes === 0) return \"0 B\"\n  const units = [\"B\", \"KB\", \"MB\", \"GB\"]\n  const index = Math.min(\n    Math.floor(Math.log(bytes) / Math.log(1024)),\n    units.length - 1\n  )\n  return `${(bytes / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`\n}\nfunction matchesAccept(file: File, accept?: string) {\n  if (!accept) return true\n  return accept.split(\",\").some((rawToken) => {\n    const token = rawToken.trim().toLowerCase()\n    if (!token) return false\n    if (token.startsWith(\".\")) return file.name.toLowerCase().endsWith(token)\n    if (token.endsWith(\"/*\")) {\n      return file.type.toLowerCase().startsWith(token.slice(0, -1))\n    }\n    return file.type.toLowerCase() === token\n  })\n}\nfunction toUploadItems(files: FileList | File[]): FileUploadItem[] {\n  return Array.from(files).map((file) => ({\n    id: `${file.name}-${file.size}-${file.lastModified}`,\n    name: file.name,\n    type: file.type || \"Unknown type\",\n    size: file.size,\n    url: URL.createObjectURL(file),\n  }))\n}\nfunction UploadIconCluster({\n  acceptedFileTypes,\n  isDragging,\n}: {\n  acceptedFileTypes: AcceptedFileType[]\n  isDragging: boolean\n}) {\n  const singleIcon = acceptedFileTypes.length === 1\n  return (\n    <div className=\"relative h-14 w-36\">\n      {acceptedFileTypes.map((item, index) => (\n        <Card\n          key={item.label}\n          className={cn(\n            \"absolute top-1/2 left-1/2 grid size-12 place-items-center rounded-xl bg-background text-muted-foreground transition-[transform,color,background-color] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] before:rounded-[calc(var(--radius-xl)-1px)]\",\n            \"motion-reduce:transition-none\",\n            index === 1 && \"z-10\",\n            isDragging &&\n              \"bg-popover text-foreground shadow-md shadow-black/10 not-dark:bg-clip-border dark:shadow-black/25\"\n          )}\n          style={{\n            transform: singleIcon\n              ? `translate(-50%, -50%) scale(${isDragging ? 1.14 : 1})`\n              : isDragging\n                ? ICON_TRANSFORMS[index]?.active\n                : ICON_TRANSFORMS[index]?.idle,\n          }}\n        >\n          <item.icon className=\"size-5\" />\n        </Card>\n      ))}\n    </div>\n  )\n}\nexport function FileUpload({\n  accept = DEFAULT_ACCEPT,\n  acceptedFileTypes = ACCEPTED_FILE_TYPES,\n  borderBeamTheme = \"light\",\n  browseLabel = \"Browse files\",\n  className,\n  description = \"PDF, DOC/DOCX, XLSX, CSV, PNG, or JPG\",\n  draggingLabel = \"Drop to add\",\n  multiple = true,\n  showBorderBeam = true,\n  showFileList = true,\n  title = \"Click to upload or drop files\",\n  onFilesAccepted,\n  onFilesChange,\n}: FileUploadProps) {\n  const dragDepthRef = React.useRef(0)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [isDragging, setIsDragging] = React.useState(false)\n  const [files, setFiles] = React.useState<FileUploadItem[]>([])\n  const [rejectionMessage, setRejectionMessage] = React.useState<string | null>(\n    null\n  )\n  const commitFiles = React.useCallback(\n    (nextFiles: FileList | File[]) => {\n      const acceptedFiles = Array.from(nextFiles)\n        .filter((file) => matchesAccept(file, accept))\n        .slice(0, multiple ? undefined : 1)\n      if (acceptedFiles.length === 0) {\n        setRejectionMessage(\"This file type is not supported here.\")\n        return\n      }\n      setRejectionMessage(null)\n      onFilesAccepted?.(acceptedFiles)\n      const items = toUploadItems(acceptedFiles)\n      setFiles((previousFiles) => {\n        previousFiles.forEach((file) => URL.revokeObjectURL(file.url))\n        return items\n      })\n      onFilesChange?.(items)\n    },\n    [accept, multiple, onFilesAccepted, onFilesChange]\n  )\n  React.useEffect(() => {\n    return () => {\n      files.forEach((file) => URL.revokeObjectURL(file.url))\n    }\n  }, [files])\n  const openFileDialog = React.useCallback(() => {\n    inputRef.current?.click()\n  }, [])\n  const dropzone = (\n    <div\n      role=\"button\"\n      tabIndex={0}\n      className={cn(\n        \"relative flex min-h-64 cursor-pointer flex-col items-center justify-center gap-5 overflow-hidden rounded-[1.125rem] border border-dashed bg-background px-6 py-10 text-center transition-[border-color,background-color] duration-200 ease-out\",\n        \"motion-reduce:transition-none\",\n        isDragging\n          ? \"border-foreground/40 bg-accent/35\"\n          : \"border-foreground/20 hover:border-foreground/35 hover:bg-muted/35 dark:border-foreground/25 dark:hover:border-foreground/40\"\n      )}\n      onClick={openFileDialog}\n      onDragEnter={(event) => {\n        event.preventDefault()\n        dragDepthRef.current += 1\n        setIsDragging(true)\n      }}\n      onDragLeave={(event) => {\n        event.preventDefault()\n        dragDepthRef.current = Math.max(0, dragDepthRef.current - 1)\n        if (dragDepthRef.current === 0) setIsDragging(false)\n      }}\n      onDragOver={(event) => event.preventDefault()}\n      onDrop={(event) => {\n        event.preventDefault()\n        dragDepthRef.current = 0\n        setIsDragging(false)\n        if (event.dataTransfer.files.length > 0) {\n          commitFiles(event.dataTransfer.files)\n        }\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \"Enter\" || event.key === \" \") {\n          event.preventDefault()\n          openFileDialog()\n        }\n      }}\n    >\n      <UploadIconCluster\n        acceptedFileTypes={acceptedFileTypes}\n        isDragging={isDragging}\n      />\n      <div className=\"space-y-1\">\n        <div className=\"text-sm font-medium\">{title}</div>\n        <div className=\"text-xs text-muted-foreground\">{description}</div>\n        {rejectionMessage ? (\n          <div className=\"text-xs text-destructive\">{rejectionMessage}</div>\n        ) : null}\n      </div>\n      <div className=\"inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1 text-xs text-muted-foreground\">\n        <IconPlaceholder\n          lucide=\"Upload\"\n          tabler=\"IconUpload\"\n          hugeicons=\"Upload01Icon\"\n          phosphor=\"UploadSimpleIcon\"\n          remixicon=\"RiUpload2Line\"\n          className=\"size-3.5\"\n        />\n        <span>{isDragging ? draggingLabel : browseLabel}</span>\n      </div>\n      <input\n        ref={inputRef}\n        type=\"file\"\n        accept={accept}\n        multiple={multiple}\n        className=\"hidden\"\n        onChange={(event) => {\n          if (event.target.files) {\n            commitFiles(event.target.files)\n            event.currentTarget.value = \"\"\n          }\n        }}\n      />\n    </div>\n  )\n  return (\n    <div className={cn(\"space-y-3\", className)}>\n      {showBorderBeam ? (\n        <BorderBeam\n          active={isDragging}\n          borderRadius={18}\n          brightness={2.4}\n          className=\"rounded-[1.125rem]\"\n          colorVariant=\"ocean\"\n          duration={2.4}\n          size=\"md\"\n          strength={1}\n          theme={borderBeamTheme}\n        >\n          {dropzone}\n        </BorderBeam>\n      ) : (\n        dropzone\n      )}\n      {showFileList && files.length > 0 ? (\n        <div className=\"rounded-xl border bg-background\">\n          {files.map((file) => (\n            <div\n              key={file.id}\n              className=\"flex items-center gap-3 border-b px-3 py-2.5 last:border-b-0\"\n            >\n              <FileThumbnail\n                file={{\n                  name: file.name,\n                  type: file.type,\n                }}\n                previewImageUrl={\n                  file.type.startsWith(\"image/\") ? file.url : null\n                }\n                className=\"size-10 shrink-0 rounded-lg\"\n              />\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"truncate text-sm font-medium\">{file.name}</div>\n                <div className=\"truncate text-xs text-muted-foreground\">\n                  {file.type} - {formatBytes(file.size)}\n                </div>\n              </div>\n              <div className=\"rounded-full bg-muted px-2 py-1 text-xs text-muted-foreground\">\n                Ready\n              </div>\n            </div>\n          ))}\n        </div>\n      ) : null}\n    </div>\n  )\n}\ntype InlineRegistryIconProps = Omit<\n  React.ComponentProps<\"svg\">,\n  \"children\" | \"strokeWidth\"\n> & { strokeWidth?: number }\n",
      "type": "registry:ui",
      "target": "@components/extend/file-upload.tsx"
    }
  ],
  "categories": [
    "documents"
  ],
  "type": "registry:ui"
}
