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

# `lr-attachment-trigger`

- **Import** `import '@aceshooting/lyra-ui/components/lr-attachment-trigger.js';` (stable tag alias; registers the tag)
- **Class** `LyraAttachmentTrigger`, also available unregistered from `@aceshooting/lyra-ui/components/media/attachment-trigger/attachment-trigger.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)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 7 parts, 0 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-attachment-trigger`

A compact attach affordance designed for a chat composer's start slot (see `lr-chat-composer`'s
own `start` slot, which this drops straight into, though it has no code dependency on it). First-
party invention (no Web Awesome equivalent). Its shape adapts to how many attachment `capabilities`
are configured: exactly one renders a single plain icon button; more than one renders a small
anchored menu (composed from `lr-dropdown`/`lr-menu`/`lr-menu-item`) listing each
capability as a row.

**Properties:**

- `capabilities: readonly LyraAttachmentCapability[] = ['files']` (property only, no attribute) —
  which capabilities to offer, in display order. `LyraAttachmentCapability = 'files' | 'image' |
'camera' | 'audio'`; `LyraFileBackedCapability = 'files' | 'image'` (the two that actually open
  the file picker). Writes inspect at most the first 64 candidates and normalize to a frozen,
  deduplicated, at-most-four entry snapshot. Unknown values and duplicates do not consume that
  four-entry output budget; hostile/invalid collections fail closed to the default.
- `accept: string = ''` — a native-file-input-style accept string (e.g. `'image/*'` or
  `'.pdf,.docx'`), forwarded to the hidden file input for the `files`/`image` capabilities. `image`
  defaults it to `'image/*'` unless this prop overrides it; `files` always uses it as-is (empty
  means "any file type").
- `multiple: boolean = true` (reflected) — forwarded to the hidden file input's own `multiple`
  attribute.
- `disabled: boolean = false` (reflected)
- `accessibleLabel?: string` (attribute `accessible-label`) — overrides either trigger shape's
  localized accessible-name fallback. A host `aria-label`, including explicit empty, wins.
- `triggerTitle?: string` (attribute `trigger-title`) — forwards a sighted-user hover tooltip to
  both the single-capability and multi-capability trigger buttons
- `appearance: LyraAppearance = 'plain'` (reflected) — how the trigger fills itself, from the
  library's shared `accent`/`filled`/`outlined`/`filled-outlined`/`plain` vocabulary. `'plain'` is
  exactly the treatment this component shipped before it had the property
- `size: LyraSize = 'm'` (reflected) — size on the shared six-step ladder, accepting the
  `small`/`medium`/`large` spellings too. The tier scales the **glyph**, never the tappable box:
  `--lr-icon-button-size` is an accessibility floor and the ladder's tightest steps resolve below
  WCAG 2.5.8's minimum. Override `--lr-icon-button-size` to make that trade-off explicitly

**Events:** `lr-files` (`detail: { capability: 'files' | 'image'; files: readonly File[] }`) — fired
once a file-backed capability's hidden input produces a real selection. `files` is a fresh frozen
owner-realm array snapshot, not a live reference to the input's own `.files`. `lr-camera-request`
and `lr-audio-request`
(both no detail — `detail` is `null`, not `undefined`, per the DOM spec's `CustomEventInit`
default) — fired when the `camera` / `audio` capability is activated; this component implements no
capture UI of its own, the host owns everything from here (there's no single right answer for
`getUserMedia` vs. `<input capture>` vs. a native wrapper's own camera API; for `audio` the
typical host response is opening `<lr-push-to-talk>` in an overlay, then handing the resulting
blob to `<lr-attachment-chip>`). `focus`/`blur` from the active single- or multi-capability trigger
are relayed exactly once as owner-realm native `FocusEvent`s (bubbling and composed, preserving
`relatedTarget`); the hidden file input is not the focus owner.
The composed dropdown/menu implementation lifecycle, item-state, and selection events are
contained inside the trigger. Only the attachment events listed above cross the host boundary.

**Slots:** none — capabilities are configured entirely via the `capabilities` prop.

**CSS parts:** `trigger` (the single-capability button, only rendered when
`capabilities.length === 1`), `menu` (the `lr-dropdown` shell, only rendered when
`capabilities.length > 1`), `menu-trigger` (the multi-capability button slotted into `lr-dropdown`'s
`trigger` slot, only rendered when `capabilities.length > 1`), `expand-icon` (the disclosure chevron
inside the multi-capability trigger button, only rendered when `capabilities.length > 1`),
`trigger__control` / `menu-trigger__control` (each trigger's own native `<button>` — as of 16.0.0
both triggers are composed `<lr-icon-button>`s, so `trigger`/`menu-trigger` name those hosts and the
painted surface sits one boundary deeper),
`hidden-input` (the internal native `<input type="file">` that actually opens the OS file picker;
hidden via CSS by default, exposed as a part only so a consumer can override that with
`::part(hidden-input)` in the unlikely case their integration needs to).

**Migrating a pre-16.0.0 `::part()` rule.** This component's icon-only action is a composed
`<lr-icon-button>`, so the part naming that action now names the composed child's HOST, which
paints nothing. A `border`, `background` or `border-radius` set on it is silently dead — only
`color` still appears to work, because it inherits, which makes such a rule look half-alive rather
than broken. Set `--lr-icon-button-background`/`-color`/`-border`/`-radius` (and their
`-hover`/`-active` variants) on this element or an ancestor instead: the composed control reads
those public tokens ahead of any default this component supplies. For SIZE use
`--lr-theme-icon-button-size`, not `--lr-icon-button-size` — every `LyraElement` re-declares the
latter on its own `:host`, so it never reaches a composed child (see `llms/tokens.md`).

**An ancestor's public border wins over this component's own relayed default, not just its
absence.** Unlike most composing components, the `outlined`/`filled-outlined` appearances here DO
relay a non-zero `--_lr-icon-button-border-default` (a themed edge) into the trigger's private
fallback tier. That does not change the resolution order: `--lr-icon-button-border` (and its
`-hover`/`-active` variants) is still the FIRST arm of the token chain, read by the composed
trigger ahead of whatever this component relays, so setting it on this element or an ancestor
overrides the outlined edge exactly as it overrides the `plain`/`filled`/`accent` appearances'
zero default. The private relay is only this component's own default opinion, never a gate the
public token must pass through. Size remains the one exception that does not cross this way: use
`--lr-theme-icon-button-size`, never `--lr-icon-button-size`, as noted above.

**Themeable custom properties:** shared tokens only — `--lr-space-xs`, `--lr-color-text`/
`-text-quiet`, `--lr-icon-button-size`, `--lr-focus-ring-*`, `--lr-opacity-disabled`,
`--lr-radius`, `--lr-transition-fast`.

**Optional peer deps:** none.

```html
<lr-attachment-trigger accept=".pdf,.docx"></lr-attachment-trigger>
<script type="module">
  const trigger = document.querySelector("lr-attachment-trigger");
  trigger.capabilities = ["files", "image", "camera"];
  trigger.addEventListener("lr-files", (e) => queueFiles(e.detail.capability, e.detail.files));
  trigger.addEventListener("lr-camera-request", openCameraFlow);
</script>
```

**Known gotchas:**

- `HTMLInputElement.files` is a _live_ view in most browsers — clearing `input.value` after reading
  `.files` (needed so re-picking the exact same file still fires another `change` event next time)
  mutates that exact `FileList` object back to empty in place, not just detaches a stale reference.
  A consumer reading `lr-files` later would otherwise observe an empty list — this component
  copies into a fresh frozen `File[]` before clearing the native input.
- The `camera`/`audio` capabilities never touch the hidden `<input type="file">` at all — both are
  scope-limited by design to firing `lr-camera-request`/`lr-audio-request` and nothing else. The
  hidden input is only rendered when `capabilities` contains `files` or `image`.
- Setting `disabled` closes an open capability menu, disables its items as well as the trigger, and
  discards a native file selection that arrives after the component became disabled.

**Additional API surface:**

- `click()` — Activates the internal attachment trigger.

---
