{
  "name": "FileUpload",
  "source": "src/components/FileUpload.tsx",
  "propsType": "FileUploadProps",
  "description": "FileUpload — a drop target wrapped around a real `<input type=\"file\">`.\n\nDrawn in Figma as `File Upload` (1367:2333) — two sizes by five states. The\nmeasurements came from `Input` and `EmptyState` rather than being invented:\nthe border, radius and disabled treatment are Input's; the centred\nicon-over-text stack is EmptyState's. Figma draws the drop zone alone, the\nsame split `Input` makes against `Form Field`.\n\nTHE INPUT IS THE CONTROL. THE DROP ZONE IS DECORATION.\n\nThe usual build of this component is a `<div>` with drag handlers and a\nclick that calls `input.click()`. That version cannot be reached by keyboard,\nhas no accessible name, does not participate in a form, and does not work in\nany environment without a pointer — which includes switch access, voice\ncontrol and most screen-reader browse modes.\n\nHere the file input is a real, focusable, labelled control that is visually\nhidden but NOT `display: none` — it keeps its place in the tab order and its\nlabel. Drag-and-drop is layered on top as an enhancement, and every path it\noffers is also reachable without it. That ordering is the whole component.\n\nVALIDATION IS ADVISORY, NOT SECURITY. `accept` and `maxSize` are checked here\nso the user finds out immediately instead of after an upload. A server that\ntrusts either one is trusting a value the client chose.",
  "import": "import { FileUpload } from 'ionbase-ui';",
  "status": "stable",
  "summary": "A drop target wrapped around a real file input, with client-side accept and size checks.",
  "useWhen": [
    "the user attaches one or more files to a form or a message",
    "the constraints (types, size, count) should be stated before the upload is attempted rather than after"
  ],
  "useInstead": [
    {
      "when": "a single file is picked and no drop target or file list is wanted",
      "use": "a plain <input type=\"file\">",
      "why": "this component adds a drop zone and a removable list; without either it is a heavier way to render the same control"
    },
    {
      "when": "upload progress needs to be shown",
      "use": "ProgressBar",
      "why": "FileUpload selects files; it does not transfer them, and deliberately owns no network state"
    }
  ],
  "variants": {
    "size": {
      "sm": {
        "use": "inside a dense form, or beside other fields"
      },
      "md": {
        "use": "the default — a standalone attachment area"
      }
    }
  },
  "a11y": {
    "guarantees": [
      "the control is a real <input type=\"file\">, focusable and in the tab order — the drop zone is an enhancement layered on top, never the only way in",
      "the zone is a <label> for that input, so clicking anywhere opens the picker without any script",
      "the input is clipped to 1px rather than display:none, which would remove it from the tab order and the accessibility tree",
      "the focus ring is drawn on the zone, where the user is looking, not on the clipped input",
      "a polite live region announces the number of files held, because a drop changes nothing near the user's focus",
      "dragging is signalled by a solid border as well as a colour, so it is not conveyed by colour alone (WCAG 1.4.1)",
      "each remove button is a 24px target with a name that includes the file (WCAG 2.5.8)"
    ],
    "requires": [
      "`label`, or an `aria-label` — an unnamed file input announces only as \"button\"",
      "`hint` describing the accepted types and size limit, since `accept` itself is never announced"
    ]
  },
  "antiPatterns": [
    {
      "dont": "treating `accept` or `maxSize` as validation",
      "why": "both are client-side and advisory. A browser does not enforce `accept` on drop, and every platform picker offers an \"All files\" escape. The server must check again"
    },
    {
      "dont": "rebuilding this as a div with onClick={() => inputRef.current.click()}",
      "why": "that version has no accessible name, no tab stop, no form participation and no keyboard path — it is the usual build of this component and the usual defect"
    },
    {
      "dont": "putting the file list inside the drop zone",
      "why": "the zone is a label, so every row in it — including each remove button — would also open the file picker"
    },
    {
      "dont": "using `onChange` as a delta",
      "why": "it fires with the complete list after every add and every remove, never with what changed"
    }
  ],
  "slots": {
    "prompt": {
      "expects": "the call to action inside the zone"
    },
    "hint": {
      "expects": "the constraints in words — \"PNG or JPG, up to 5 MB\""
    }
  },
  "stylesheet": "src/styles/file-upload.css",
  "tokens": [
    "--border-default",
    "--border-disabled",
    "--border-error",
    "--border-focus",
    "--border-strong",
    "--border-width-default",
    "--border-width-thick",
    "--font-family-sans",
    "--font-weight-medium",
    "--icon-default",
    "--icon-disabled",
    "--icon-secondary",
    "--icon-size-lg",
    "--icon-size-md",
    "--icon-size-sm",
    "--ion-duration-base",
    "--ion-ease-out",
    "--ion-file-upload-gap",
    "--ion-file-upload-icon-size",
    "--ion-file-upload-padding",
    "--radius-md",
    "--radius-sm",
    "--ring-focus",
    "--spacing-12",
    "--spacing-20",
    "--spacing-24",
    "--spacing-32",
    "--spacing-6",
    "--spacing-8",
    "--surface-default",
    "--surface-disabled",
    "--surface-hover",
    "--surface-primary-subtle",
    "--surface-subtle",
    "--text-default",
    "--text-disabled",
    "--text-tertiary",
    "--type-body",
    "--type-body-line-height",
    "--type-body-sm",
    "--type-body-sm-line-height"
  ],
  "props": {
    "label": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Field label. Also the accessible name of the file input."
    },
    "description": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Helper text below the control."
    },
    "errorMessage": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Replaces the helper text when `isInvalid` is set."
    },
    "isInvalid": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own"
    },
    "isDisabled": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own"
    },
    "size": {
      "type": "FileUploadSize | undefined",
      "required": false,
      "origin": "own",
      "description": "Matches the Figma `Size` variant when drawn. Height of the drop target.",
      "values": [
        "sm",
        "md"
      ]
    },
    "accept": {
      "type": "string | undefined",
      "required": false,
      "origin": "own",
      "description": "Native `accept` — a comma-separated list of extensions or MIME types."
    },
    "multiple": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own",
      "description": "Allow more than one file."
    },
    "maxSize": {
      "type": "number | undefined",
      "required": false,
      "origin": "own",
      "description": "Reject anything larger, in bytes."
    },
    "maxFiles": {
      "type": "number | undefined",
      "required": false,
      "origin": "own",
      "description": "Reject once this many files are held. Only meaningful with `multiple`."
    },
    "files": {
      "type": "readonly File[] | undefined",
      "required": false,
      "origin": "own",
      "description": "The files held. Pass it to control the list; omit and the component keeps\nits own. A controlled list is the usual case, because the caller is the\nonly thing that knows which uploads have since succeeded or failed."
    },
    "onChange": {
      "type": "((files: File[]) => void) | undefined",
      "required": false,
      "origin": "own",
      "description": "Fires with the full list after every add or remove, never with a delta."
    },
    "onReject": {
      "type": "((rejected: RejectedFile[]) => void) | undefined",
      "required": false,
      "origin": "own",
      "description": "Fires with anything refused by `accept`, `maxSize` or `maxFiles`."
    },
    "prompt": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Call-to-action inside the drop target."
    },
    "hint": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Second line inside the drop target — the constraints, in words."
    },
    "removeLabel": {
      "type": "((name: string) => string) | undefined",
      "required": false,
      "origin": "own",
      "description": "Accessible label for each file's remove button. Receives the file name."
    },
    "labels": {
      "type": "FileUploadLabels | undefined",
      "required": false,
      "origin": "own",
      "description": "Every other string the control renders, for translation. English by default."
    },
    "className": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "wrapperClassName": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "id": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "name": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    }
  },
  "propCounts": {
    "own": 21,
    "aria": 0,
    "dom": 0,
    "other": 0
  }
}
