<!-- GENERATED by scripts/build-llms.mjs from llms/media.md — do not edit this file. -->

# `lr-file-input`

- **Import** `import '@aceshooting/lyra-ui/components/lr-file-input.js';` (stable tag alias; registers the tag)
- **Class** `LyraFileInput`, also available unregistered from `@aceshooting/lyra-ui/components/media/file-input/file-input.class.js`
- **Family** `components/media/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecated part** `base` since `8.2.3`; use part `::part(file-input)`; removal not before `10.0.0` — The file-input part names the interactive picker surface; base remains on that same node during the compatibility window. That version is a policy floor, not a plan: `wa-file-input` still publishes its own deprecated `base` part, so this alias is removed only when upstream's is.
- **Deprecated part** `label` since `8.2.3`; use part `::part(form-control-label)`; removal not before `10.0.0` — The form-control-label part follows the shared form-control vocabulary; label remains on that same node during the compatibility window. That version is a policy floor, not a plan: `wa-file-input` still publishes its own deprecated `label` part, so this alias is removed only when upstream's is.
- **Optional peers** none
- **Themeable via** 22 parts, 21 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-file-input`

A form-associated drag-drop + click-to-browse file dropzone. It stores and renders raw `File[]`;
no client-side CSV/XLSX/etc. parsing is performed (that's left entirely to the host).

Removing `label` or `hint` consumes it as absent without changing the property's `null` readback. An
explicit empty string remains empty. Text chrome and label/description associations disappear when
no corresponding slot or explicit presence hint remains, and a later value restores them. Removing
the label leaves the localized dropzone instruction available. Disabled file removal buttons,
including fieldset disablement, retain resting foreground and background under hover and press;
enabled buttons retain pointer feedback.

**Properties:**

- `multiple: boolean = false` (reflected)
- `disabled: boolean = false` (reflected)
- `files: File[] = []` — selected files; programmatic writes are event-silent and immediately
  synchronize rendering, validity, and form submission
- `readonly fileCount: number` and `readonly dragging: boolean` — derived state. Assigning `files`
  updates the selected-file count; real drag events alone own the drag session and its
  accept/reject state.
- `name: string | null = null`, `required: boolean = false`, `form`, `labels`, `validity`,
  `validationMessage`, and `willValidate` — standard form-associated surface. One file submits as a
  `File`; `multiple` submits repeated entries under `name`
- `customError: string | null` (attribute `custom-error`) — reflected consumer validation message;
  a non-empty value blocks submission until cleared with `setCustomValidity('')` or
  `resetValidity()`
- `accept: string = ''` — a native-`accept`-style string (`.csv,.xlsx`, `text/csv`, `image/*`, or any
  comma-separated mix); now enforced on **both** the native picker dialog and the drag-drop path, see
  gotchas
- `capture: '' | 'user' | 'environment' = ''` — forwarded to the native file picker
- `allowedMimeTypes: readonly string[] = []` (attribute: false) — exact MIME-string allowlist
- `forbiddenMimeTypes: readonly string[] = []` (attribute: false) — exact MIME-string denylist,
  checked **before** (and taking precedence over) `allowedMimeTypes`. Both properties take frozen
  snapshots, retain valid string entries, and inspect at most 10,000 candidates per assignment;
  update them by assigning a new collection.
- `maxFileSize: number = 0` (attribute `max-file-size` — bytes; `0` disables the check)
- `maxFiles: number = 0` (attribute `max-files`) — largest total file count accepted, counting
  retained files (unless `nonRetaining`) plus `heldFileCount` plus the current batch; `0` disables
  the check. An excess file in the batch is rejected with reason `'maxFiles'`, in the same
  `[part="rejection"]` shape as `maxFileSize`.
- `maxTotalSize: number = 0` (attribute `max-total-size`) — largest combined byte size accepted,
  summing retained files (unless `nonRetaining`) plus `heldTotalSize` plus the current batch; `0`
  disables the check. Same rejection-UI shape and fail-safe invalid-override behavior as
  `maxFileSize` (see gotchas).
- `heldFileCount: number = 0` (attribute `held-file-count`) — externally held file count added to
  the running count `maxFiles` evaluates against, in both retaining and `nonRetaining` modes — the
  numeric counterpart of `valuePresent`, for a cumulative cap (e.g. a server-backed upload limit)
  that spans separate picker sessions rather than resetting to what this control alone can see. `0`
  (the default) means "nothing held" and reproduces prior behavior exactly. A negative, `NaN`, or
  `Infinity` override is normalized to `0` via `finiteCount` rather than corrupting every later
  comparison or permanently blocking every future file.
- `heldTotalSize: number = 0` (attribute `held-total-size`) — externally held byte total added to
  the running size `maxTotalSize` evaluates against, in both retaining and `nonRetaining` modes.
  Same contract, default, and invalid-input normalization as `heldFileCount`.
- `nonRetaining: boolean = false` (reflected, attribute `non-retaining`) — opt-in mode where an
  accepted selection still fires `lr-files`/`input`/`change` but is never written to `files` or
  rendered as a built-in `[part="file"]` row. For a host that persists files elsewhere and renders
  its own list, so assigning `files` (even to reset it) never fights that host-owned rendering.
  `required` validity and the `blank` custom state then read `valuePresent` instead of
  `files.length`. Does not affect `formStateRestoreCallback()` or a direct `files` assignment, both
  of which still retain.
- `valuePresent: boolean = false` (reflected, attribute `value-present`) — external "a value is
  present" signal a `nonRetaining` host sets once it has taken ownership of the selected files, so
  `required` validity and the `blank` state reflect externally-held files. Ignored while
  `nonRetaining` is `false`.
- `directory: boolean = false` (reflected) — enables native directory selection where supported
- `paste: boolean = true` (reflected) — accepts files pasted into the dropzone
- `compact: boolean = false` (reflected) — tighter dropzone padding, gap and label font for
  constrained spaces (a toolbar, a table cell) — the same convention as `lr-empty`'s `compact`. The
  dashed border stays; only the internal spacing shrinks. `false` (the default) keeps the full
  `--lr-space-l` dropzone.
- `label: string = ''` and `hint: string = ''`; an empty `label` leaves the localized dropzone
  instruction (`fileInputDefaultLabel`) as the visible fallback
- `errorText: string = ''` (attribute `error-text`) — plain-text owned validation feedback. When
  it is empty, a `customError` message is rendered when present; otherwise an intrinsic validation
  message appears after the control has been interacted with. Rich `error` slot content replaces
  that text.
- `withLabel: boolean = false`, `withHint: boolean = false`, and `withError: boolean = false`
  (attributes `with-label`, `with-hint`, and `with-error`) — SSR slot-presence hints. Use
  `with-error` when the rich `error` slot is populated in initial declarative-shadow-DOM output,
  before hydration can observe the assigned light-DOM content.
- `size: LyraSize = 'm'` (reflected)
- `validators: LyraFileInputValidator[] = []` (attribute: false) — additional JavaScript
  constraints, run after the intrinsic `required` check. **Fixed in 9.0.0:** the property was
  previously declared (typed `unknown[]`) and read by nothing, so an assigned validator silently
  never ran. It now implements the same contract as `lr-date-input`/`lr-combobox`:
  - a function `(files: File[], input: LyraFileInput) => void | boolean | string | ValidityStateFlags`
    — `undefined`/`true` passes, a string is the validation message (raising `customError`), `false`
    is a generic failure using the localized `valueInvalid` string, and a `ValidityStateFlags` object
    names the flags to raise;
  - an object with `validate(files, input)` returning the same shapes;
  - the mapped object-validator shape `{ observedAttributes?, checkValidity(input), message? }`,
    whose `checkValidity()` returns `{ isValid, invalidKeys, message }`. Unrecognized `invalidKeys`
    are dropped, and an empty mapped set synthesizes `customError`. The message falls back from the
    result's own `message` to the validator's static or function `message`, then to the localized
    default. Attributes listed in `observedAttributes` are watched on the host and revalidate live.
    Validators run in order and the first failure wins. A validator that throws fails closed with the
    localized generic message rather than escaping into the caller. Unreadable proxy-backed
    validator collections/results are contained too: a collection or result trap fails closed,
    returned validity flags are copied through the native flag vocabulary, and an unreadable
    `observedAttributes` list is skipped without rejecting the component's update. The public array
    remains the live mutable mirrored contract; each validation/observer pass takes only a transient
    iteration snapshot. `checkValidity()` and `reportValidity()` recompute at call time, so a
    validator that starts failing without any host property changing is still seen. Own or
    fieldset-cascaded `disabled` bars configured validators exactly as it bars the intrinsic
    constraint. Exported types:
    `LyraFileInputValidator`, `LyraFileInputValidatorResult`, `LyraFileInputObjectValidator`,
    `LyraFileInputObjectValidatorResult`.
- `validationTarget: HTMLElement | undefined` — the focusable base of the dropzone control after
  first render. Assign another shadow descendant to override where native constraint-validation UI
  is anchored; assign `undefined` to restore the default focusable base
- `accessibleLabel: string = ''` (attribute `aria-label`) — overrides `label` as the internal
  dropzone/button accessible name without changing visible copy
- `acceptedMessage?: string` (attribute `accepted-message`) — live-region message after an
  accepted selection; `{count}` is replaced with the accepted count. Absence uses the localized
  singular/plural `fileInputAcceptedOne`/`fileInputAcceptedMany` default. Every explicit string,
  including empty and the former `'{count} file(s) added.'` English default, wins verbatim.
- `rejectedMessage?: string` (attribute `rejected-message`) — live-region message after rejected
  files; `{count}` is replaced with the rejected count. Absence uses the localized singular/plural
  `fileInputRejectedOne`/`fileInputRejectedMany` default. Every explicit string, including empty
  and the former `'{count} file(s) rejected.'` English default, wins verbatim.

**Methods:** `openPicker()` programmatically opens the native file dialog; `focus(options?)`,
`blur()`, and `click()` forward to the interactive dropzone. Standard FACE methods are
`getForm()`, `checkValidity()`, `reportValidity()`, `setCustomValidity(message)`, and
`resetValidity()`; reset clears only consumer custom validity and restores current intrinsic
`required` validity.

**Events:** a user selection or removal emits native bubbling/composed `input`, then exactly one
host `change`; programmatic `files` writes are silent (both still fire while `nonRetaining` is set,
even though `files` itself is not written in that mode). `lr-files` (`detail:
LyraFileInputFilesDetail`, with fresh frozen readonly `files` and `rejected` arrays and frozen
rejected-file records, plus `remainingFiles`/`remainingTotalSize` reporting the allowance still left
under `maxFiles`/`maxTotalSize` after this batch (`null` while that limit is unset), fired on both
drop and manual file-picker selection; immutable `File` objects retain identity) — typed as
`LyraFileInputFilesEvent`, so `event.target`/`event.currentTarget` are `LyraFileInput` without a
cast. `LyraFileInputRejectedFile = { readonly file: File; readonly reason:
'type' | 'count' | 'size' | 'directory' | 'read' | 'limit' | 'maxFiles' | 'maxTotalSize' }`: `'type'`
from `accept`/`allowedMimeTypes`/`forbiddenMimeTypes`, `'count'` when a single-file input
(`multiple` unset) receives more than one file (in which case _all_ files are rejected, none
accepted), `'size'` from `maxFileSize`, `'maxFiles'`/`'maxTotalSize'` from the two aggregate limits,
`'directory'` for a dropped folder in single-file mode, `'read'` when a file/directory reader fails,
or `'limit'` when folder traversal exceeds its bounded entry budget. Read/limit failures reject the
complete selection atomically; lifecycle cancellation and supersession stay silent. `focus`/`blur`
fire when the semantic dropzone (the actual keyboard-focusable element, not the hidden native
`<input>`) gains/loses focus. `lr-invalid` is the bubbling/composed alias of native invalidity.

Each rejected file also renders as its own line in the visible `[part="rejection"]` region, naming
the file and the reason via one of eight locale keys: `fileInputRejectedType` (default
`'{filename}: this file type is not accepted.'`), `fileInputRejectedSize` (default
`'{filename}: this file is too large.'`), `fileInputRejectedCount` (default `'{filename}: only one
file can be selected at a time.'`), `fileInputRejectedMaxFiles` (default `'{filename}: the maximum
number of files has been reached.'`), `fileInputRejectedMaxTotalSize` (default `'{filename}: adding
this file would exceed the total size limit.'`), and — for `'directory'` — the pre-existing
`fileInputFolderRejected` (default `'Folders are not accepted here.'`, reused verbatim, so it has no
`{filename}` placeholder). Terminal traversal failures use `fileInputRejectedRead` (default
`'{filename}: the file could not be read.'`) and `fileInputRejectedLimit` (default
`'{filename}: the folder contains too many entries.'`). The filename is interpolated as
caller-supplied data, never localized itself. The same per-reason text is announced assertively;
the region is cleared (and unrendered) as soon as a subsequent selection rejects nothing.

**Slots:** `dropzone` (with the default slot retained as its fallback) supplies custom dropzone
content; `label`, `hint`, and `error` supply form chrome. The semantic button's accessible name comes from
`accessibleLabel`/host `aria-label`,
then `label`, so icon-only slot content still announces correctly. Slotted content is a sibling of
the button rather than nested inside it: links, buttons, inputs, and other interactive slotted
controls keep their own activation and do not also open the picker; clicking non-interactive custom
content still activates the dropzone.

The semantic button describes its rendered owned error and hint in that order. A supplied
`errorText` or `error` slot marks it `aria-invalid="true"`; a required intrinsic message appears
after interaction, while a custom validity message is immediately visible and survives native form
reset until `resetValidity()` or `setCustomValidity('')` clears it.

**CSS parts:** `file-input` (compatibility alias) and `form-control` (the complete form-control
frame), `form-control-label`, `label`, `hint`, `error`, `dropzone`, `dropzone-icon`, `dropzone-text`,
`base` (the native dropzone button, visually behind but semantically beside the
slotted content), `input`, `file-list`, `file`, `file-thumbnail`, `file-image`, `file-icon`,
`file-details`, `file-name`, `file-size`, `remove-button`, `status` (a visually-hidden,
`aria-hidden` mirror of the drag accept/reject state and the aggregate accepted/rejected count),
`rejection` (a **visible** region, rendered only while a rejection exists, listing each
currently-rejected file next to a per-reason message — in addition to, never in place of, the
sr-only `status` mirror above)

**Both live regions moved out of the shadow root (public surface change).** Neither `status` nor
`rejection` carries a live-region role any more: `status` is an `aria-hidden` mirror, and
`rejection` is plain visible text. A live region inside a shadow root is not reliably announced
(JAWS with Firefox ignores one outright), so the announcements now go to the library's shared
**light-DOM** regions appended to the consumer's `<body>` and marked
`data-lr-live-region="polite"` / `data-lr-live-region="assertive"` — the drag/selection summary
politely, and a rejection assertively, so it still interrupts.

What this changes for you:

- **Nothing about what the visible text says or where it renders.** `[part="rejection"]`'s text is
  ordinary visible content and stays in the accessibility tree, so a user who reaches it reads it
  normally. Both parts remain the right styling hooks.
- **Read announcements from the shared light-DOM region** — query
  `[data-lr-live-region="assertive"]` or `[data-lr-live-region="polite"]` in the document rather
  than the styling parts.
- **A `::part(rejection)[role]`-style selector never matched anyway** — an attribute selector
  cannot follow `::part()`. Nothing that worked before stopped working.

**CSS custom states:** `blank` and `dragging`, plus the shared validity states `required`,
`optional`, `valid`, `invalid`, `user-valid`, and `user-invalid`. As on every other form-associated
control, `valid`/`invalid` and `user-valid`/`user-invalid` stop matching entirely while the control
is barred from constraint validation (its own `disabled`, or an ancestor `<fieldset disabled>`),
matching native `:invalid`; `required`/`optional` keep publishing.

**The required marker.** With `required` set and a populated `label`, `[part="form-control-label"]`
paints the library's shared required marker — the same `::after` rule every labelled control in
the library uses, not a copy of it, so `--lr-form-control-required-content`,
`--lr-form-control-required-color` and `--lr-form-control-required-offset` retune or suppress it
here exactly as they do on `lr-input`. See `llms/shared.md` → "The required-field marker".

**Themeable custom properties:** `--lr-file-input-gap` (default `var(--lr-space-xs)`) — gap between
the dropzone's slotted children; `--lr-file-input-radius` (default `var(--lr-radius)`) — corner
radius of `[part='base']`; `--lr-file-input-compact-padding` (default `var(--lr-space-s)`) —
`[part='base']`'s padding while `compact`; `--lr-file-input-compact-gap` (default
`var(--lr-space-2xs)`) — the gap between the dropzone's slotted children while `compact`; and
`--lr-file-input-compact-font-size` (default `var(--lr-font-size-sm)`) — the label's font size while
`compact`. `--lr-file-input-font-size` (default `var(--lr-form-control-font-size)`) controls the
label and selected-filename text size.

`size` retunes the whole dropzone, not just its label: `--lr-file-input-dropzone-font-size`
(default `var(--lr-font-size-md-sm)`) for the instructional text,
`--lr-file-input-dropzone-icon-size` (default `var(--lr-font-size-xl)`) for `[part='dropzone-icon']`,
`--lr-file-input-dropzone-padding` (default `var(--lr-space-l)`) for the dropzone's own padding, and
`--lr-file-input-detail-font-size` (default `var(--lr-font-size-sm)`) for the secondary text (hint,
validation error, each file's formatted size). Each documented default is the `m`/`medium` tier, and
each is re-declared per `size` tier — so an unset or default-size control renders exactly as before,
while `size="xl"` scales the dropzone coherently instead of enlarging the label alone. `compact`
still overrides the dropzone padding and font size independently of the tier. The compact gap falls back to `--lr-file-input-gap` when its compact-specific
property is unset. The compact properties apply only while `compact` is set, so they are the way to tune a dense dropzone
without re-pointing shared spacing tokens for everything else on the page. The drag accept/reject
highlight on `[part='base'][data-drag-state='accept'|'reject']` is independently overridable too:
`--lr-file-input-accept-border-color` (default `var(--lr-color-success)`) and
`--lr-file-input-accept-bg` (default `color-mix(in srgb, var(--lr-color-success) 8%, transparent)`)
for the drag-accept state; `--lr-file-input-reject-border-color` (default `var(--lr-color-danger)`)
and `--lr-file-input-reject-bg` (default `color-mix(in srgb, var(--lr-color-danger) 8%,
transparent)`) for drag-reject. All four are inline `var()` fallbacks at the point of use, settable
on the element or any ancestor, so a consumer can retint just this dropzone's drag highlight without
hijacking the shared `--lr-color-success`/`--lr-color-danger` tokens used elsewhere.

The **resting** dropzone — the state it spends most of its life in — has the same three hooks as of
16.0.0: `--lr-file-input-dropzone-fill` (default `var(--lr-color-surface)`),
`--lr-file-input-dropzone-border-color` (default `var(--lr-color-border)`; the dashed border style
itself is unchanged), and `--lr-file-input-dropzone-hover-border-color` (default
`var(--lr-color-brand)`), which recolours the edge whether the pointer is over the button or over
the content stacked on it. The shared field halo `--lr-form-control-focus-shadow` (default `none`)
paints a `box-shadow` while the dropzone holds focus; it is additive, so the
`[part='base']:focus-visible` outline is untouched. Plus shared
tokens — `--lr-space-xs`, `--lr-space-l`,
`--lr-color-border`, `--lr-radius`, `--lr-color-surface`, `--lr-color-text-quiet`,
`--lr-focus-ring-width/-color/-offset` (`[part="base"]:focus-visible` outline),
`--lr-opacity-disabled` (`:host([disabled])` dimming).

**Optional peer deps:** none.

```html
<lr-file-input id="dataset-files" multiple accept=".csv,.xlsx"></lr-file-input>
<script>
  const input = document.querySelector("#dataset-files");
  input.allowedMimeTypes = ["text/csv"];
  input.addEventListener("lr-files", (e) => {
    console.log("accepted:", e.detail.files, "rejected:", e.detail.rejected); // rejected[i].reason
  });
</script>
```

Note: `allowedMimeTypes`/`forbiddenMimeTypes` are complex properties (`attribute: false`) — set
them via JS (`el.allowedMimeTypes = [...]`), not as a JSON string attribute.

`accept.ts` exports `matchesAccept(file, accept, assumeExtensionMatch?)` (internal — not
re-exported from the package root) — parses the same three `accept` forms the browser's native
picker accepts (extension, exact MIME, `type/*` wildcard) and reports whether a `File` matches. Used
both for the drop path's real rejection and for the dragenter preview, where a `DataTransferItem` (no
`.name`, only `.type`) can't evaluate an extension pattern yet — `assumeExtensionMatch: true` treats
that as a possible match during preview so the drag-over UI doesn't flash a false "reject" state for
an extension-only `accept` list.

**Known gotchas:**

- Paste-from-clipboard **is** supported and on by default: a `paste` event on the dropzone reads
  `e.clipboardData.files` and routes it through the same accept/reject classification as a drop.
  Set `paste="false"` (or `.paste = false`) to opt out.
- Dragged folders are traversed recursively in `multiple` mode with a 10,000-entry budget. A read
  failure or over-budget traversal rejects the complete drop atomically and emits one `lr-files`
  result with `rejected[].reason === 'read'` or `'limit'`, plus dedicated visible and assertive
  localized feedback. Lifecycle cancellation and supersession remain silent and emit no partial
  result. In single-file mode a folder is reported as `rejected[].reason === 'directory'` (paired
  with a synthetic zero-byte `File` carrying the folder name).
- `maxFileSize` fails safe rather than open: `0` (the default) or `Infinity` mean "no limit", but a
  `NaN`/negative value — an unparsable `max-file-size` attribute, or a config that hasn't loaded
  yet — falls back to a 25 MB cap (exported as `DEFAULT_MAX_FILE_SIZE_BYTES`) instead of disabling
  the check. `maxFiles` and `maxTotalSize` fail safe the identical way, falling back to
  `DEFAULT_MAX_FILES` (100) and `DEFAULT_MAX_TOTAL_SIZE_BYTES` (250 MB) respectively.
- The drag-session mechanics (nested-depth tracking, accept/reject preview, folder traversal) live
  in `internal/drop-session-controller.ts`, shared verbatim with `lr-drop-zone` below rather than
  reimplemented per component.
- `maxFileSize`/`accept` extension patterns can't be evaluated during the dragenter preview (no real
  `File.size`/`.name` available yet from a `DataTransferItem`) — the live preview state (border/
  background color, `status` announcement) is therefore only a best-effort hint; the authoritative
  accept/reject decision (and `rejected[].reason`) is always the one made at actual drop time.
- the click/keyboard-to-browse path is correctly operable (`role="button"`, `tabindex` 0/-1 by
  disabled, `aria-disabled`, Enter/Space handling).

---
