{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload",
  "title": "File Upload",
  "description": "The nx-ui File Upload component.",
  "dependencies": [
    "@untitledui/file-icons@^0.0.9",
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/utils",
    "@nx-ui/button",
    "@nx-ui/featured-icon"
  ],
  "files": [
    {
      "path": "components/ui/file-upload.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { FileIcon } from \"@untitledui/file-icons\"\nimport {\n  CheckCircle,\n  Trash01,\n  UploadCloud02,\n  XCircle,\n} from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { FeaturedIcon } from \"@/components/blocks/featured-icon\"\n\ntype FileIconProps = React.ComponentProps<typeof FileIcon>\n\n/**\n * FileUpload — the Untitled UI dropzone + upload-queue family, ported to nx-ui.\n *\n * `FileUpload.DropZone` is a dashed-ring drop target with a featured cloud icon\n * and click-or-drag copy. `FileUpload.ListItem` renders a queued file with its\n * type icon (from `@untitledui/file-icons`), size, a progress bar and\n * uploading / complete / failed states with delete and retry actions.\n *\n * Presentational: progress and state come in via props. The demo page owns the\n * simulated upload. Ported from the vendored UUI `application/file-upload`,\n * swapping react-aria + motion for native drag/drop handlers and CSS.\n */\n\n/** Human-readable file size, e.g. 1536 → \"1 KB\". */\nexport function getReadableFileSize(bytes: number) {\n  if (bytes === 0) return \"0 KB\"\n  const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"]\n  const i = Math.floor(Math.log(bytes) / Math.log(1024))\n  return `${Math.round(bytes / Math.pow(1024, i))} ${units[i]}`\n}\n\n// ---------------------------------------------------------------------------\n// Root + list\n// ---------------------------------------------------------------------------\n\nfunction FileUploadRoot({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"file-upload\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction FileUploadList({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"file-upload-list\"\n      className={cn(\"flex flex-col gap-3\", className)}\n      {...props}\n    />\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Drop zone\n// ---------------------------------------------------------------------------\n\nfunction FileUploadDropZone({\n  className,\n  hint = \"SVG, PNG, JPG or GIF (max. 800×400px)\",\n  disabled,\n  accept,\n  multiple = true,\n  isInvalid,\n  onFiles,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"onDrop\"> & {\n  hint?: React.ReactNode\n  disabled?: boolean\n  accept?: string\n  multiple?: boolean\n  isInvalid?: boolean\n  /** Called with the accepted files from a drop or file-input change. */\n  onFiles?: (files: File[]) => void\n}) {\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [dragging, setDragging] = React.useState(false)\n\n  const emit = (fileList: FileList | null) => {\n    if (!fileList) return\n    const files = Array.from(fileList)\n    onFiles?.(multiple ? files : files.slice(0, 1))\n    if (inputRef.current) inputRef.current.value = \"\"\n  }\n\n  const stop = (e: React.DragEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n  }\n\n  return (\n    <div\n      data-slot=\"file-upload-dropzone\"\n      data-dragging={dragging || undefined}\n      onDragOver={(e) => {\n        if (disabled) return\n        stop(e)\n        setDragging(true)\n      }}\n      onDragEnter={(e) => {\n        if (disabled) return\n        stop(e)\n        setDragging(true)\n      }}\n      onDragLeave={(e) => {\n        if (disabled) return\n        stop(e)\n        setDragging(false)\n      }}\n      onDrop={(e) => {\n        if (disabled) return\n        stop(e)\n        setDragging(false)\n        emit(e.dataTransfer.files)\n      }}\n      className={cn(\n        \"relative flex flex-col items-center gap-3 rounded-xl bg-background px-6 py-4 text-text-tertiary ring-1 ring-border transition ring-inset\",\n        dragging && \"ring-2 ring-ring\",\n        isInvalid && \"ring-2 ring-destructive\",\n        disabled && \"cursor-not-allowed bg-muted\",\n        className\n      )}\n      {...props}\n    >\n      <FeaturedIcon\n        icon={UploadCloud02}\n        color=\"gray\"\n        size=\"md\"\n        className={cn(disabled && \"opacity-50\")}\n      />\n\n      <div className=\"flex flex-col gap-1 text-center\">\n        <div className=\"flex justify-center gap-1 text-center\">\n          <input\n            ref={inputRef}\n            id={id}\n            type=\"file\"\n            aria-label=\"Upload file\"\n            className=\"peer sr-only\"\n            disabled={disabled}\n            accept={accept}\n            multiple={multiple}\n            onChange={(e) => emit(e.target.files)}\n          />\n          <Button\n            variant=\"link\"\n            size=\"sm\"\n            disabled={disabled}\n            onClick={() => inputRef.current?.click()}\n          >\n            Click to upload\n          </Button>\n          <span className=\"text-sm\">or drag and drop</span>\n        </div>\n        <p\n          className={cn(\n            \"text-xs transition\",\n            isInvalid ? \"text-error-primary\" : \"text-text-tertiary\"\n          )}\n        >\n          {hint}\n        </p>\n      </div>\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// List item\n// ---------------------------------------------------------------------------\n\nfunction FileUploadListItem({\n  className,\n  name,\n  size,\n  progress = 0,\n  status = \"uploading\",\n  type = \"empty\",\n  fileIconVariant = \"default\",\n  onDelete,\n  onRetry,\n  ...props\n}: Omit<React.ComponentProps<\"li\">, \"content\"> & {\n  name: string\n  /** File size in bytes. */\n  size: number\n  /** Upload progress 0–100. */\n  progress?: number\n  status?: \"uploading\" | \"complete\" | \"failed\"\n  type?: FileIconProps[\"type\"]\n  fileIconVariant?: FileIconProps[\"variant\"]\n  onDelete?: () => void\n  onRetry?: () => void\n}) {\n  const failed = status === \"failed\"\n  const complete = status === \"complete\"\n\n  return (\n    <li\n      data-slot=\"file-upload-item\"\n      className={cn(\n        \"relative flex gap-3 rounded-xl bg-background p-4 ring-1 ring-border transition ring-inset\",\n        failed && \"ring-2 ring-destructive\",\n        className\n      )}\n      {...props}\n    >\n      <FileIcon\n        className=\"size-10 shrink-0 dark:hidden\"\n        type={type}\n        theme=\"light\"\n        variant={fileIconVariant}\n      />\n      <FileIcon\n        className=\"size-10 shrink-0 not-dark:hidden\"\n        type={type}\n        theme=\"dark\"\n        variant={fileIconVariant}\n      />\n\n      <div className=\"flex min-w-0 flex-1 flex-col items-start\">\n        <div className=\"flex w-full min-w-0 flex-1\">\n          <div className=\"min-w-0 flex-1\">\n            <p className=\"truncate text-sm font-medium text-secondary-foreground\">\n              {name}\n            </p>\n            <div className=\"mt-0.5 flex items-center gap-2\">\n              <p className=\"truncate text-sm whitespace-nowrap text-text-tertiary\">\n                {getReadableFileSize(size)}\n              </p>\n              <span\n                aria-hidden=\"true\"\n                className=\"h-3 w-px rounded-full bg-border-primary\"\n              />\n              <span className=\"flex items-center gap-1\">\n                {complete && (\n                  <>\n                    <CheckCircle className=\"size-4 stroke-[2.5px] text-fg-success-primary\" />\n                    <span className=\"text-sm font-medium text-success-primary\">\n                      Complete\n                    </span>\n                  </>\n                )}\n                {status === \"uploading\" && (\n                  <>\n                    <UploadCloud02 className=\"size-4 stroke-[2.5px] text-fg-quaternary\" />\n                    <span className=\"text-sm font-medium text-text-quaternary\">\n                      Uploading…\n                    </span>\n                  </>\n                )}\n                {failed && (\n                  <>\n                    <XCircle className=\"size-4 text-fg-error-primary\" />\n                    <span className=\"text-sm font-medium text-error-primary\">\n                      Failed\n                    </span>\n                  </>\n                )}\n              </span>\n            </div>\n          </div>\n\n          <Button\n            variant=\"ghost\"\n            size=\"icon-xs\"\n            aria-label=\"Delete\"\n            className=\"-mt-2 -mr-2 self-start\"\n            onClick={onDelete}\n          >\n            <Trash01 />\n          </Button>\n        </div>\n\n        {!failed && (\n          <div className=\"mt-2 flex w-full items-center gap-3\">\n            <div className=\"relative h-2 flex-1 overflow-hidden rounded-full bg-bg-quaternary\">\n              <div\n                className=\"h-full rounded-full bg-primary transition-[width] duration-150 ease-linear\"\n                style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}\n                role=\"progressbar\"\n                aria-valuenow={progress}\n                aria-valuemin={0}\n                aria-valuemax={100}\n              />\n            </div>\n            <span className=\"text-sm font-medium text-secondary-foreground tabular-nums\">\n              {Math.round(progress)}%\n            </span>\n          </div>\n        )}\n\n        {failed && (\n          <Button\n            variant=\"link\"\n            size=\"sm\"\n            onClick={onRetry}\n            className=\"mt-1.5 text-destructive hover:text-destructive\"\n          >\n            Try again\n          </Button>\n        )}\n      </div>\n    </li>\n  )\n}\n\nconst FileUpload = Object.assign(FileUploadRoot, {\n  Root: FileUploadRoot,\n  List: FileUploadList,\n  DropZone: FileUploadDropZone,\n  ListItem: FileUploadListItem,\n})\n\nexport {\n  FileUpload,\n  FileUploadRoot,\n  FileUploadList,\n  FileUploadDropZone,\n  FileUploadListItem,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}