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

# `lr-time-input`

- **Import** `import '@aceshooting/lyra-ui/components/lr-time-input.js';` (stable tag alias; registers the tag)
- **Class** `LyraTimeInput`, also available unregistered from `@aceshooting/lyra-ui/components/forms/input/time-input.class.js`
- **Family** `components/forms/` — 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** 23 parts, 35 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-time-input`

A locale-aware segmented field and column picker mirroring `wa-time-input`. Its wire and submitted
value is always timezone-free, 24-hour ASCII: `HH:mm`, or `HH:mm:ss` when seconds are visible.
Locale changes segment order, separators, digits, and day-period labels — never the wire value.
Typing accepts both ASCII digits and the active locale's digit glyphs, and validation-message
bounds use the same localized time presentation rather than exposing the ASCII wire form.
An incomplete draft remains visible for editing but submits `''` and raises `badInput`.
The clear/expand actions sit directly in the shared outer height ladder: compact tiers grow only
enough for their hit targets, while `l` and `xl` retain the shared 48px and 56px heights rather
than adding outer padding around the buttons.

Removing `label` or `hint` safely omits that content while retaining native `null` property
readback. Explicit empty strings remain empty; later supplied text renders normally.

**Properties:**

- `value: string` (also accepts a `Date` or `null` when assigned) — strict `HH:mm`, optional
  `:ss`/`.sss`; `Date` reads local clock fields without timezone conversion. Invalid strings and
  `null` normalize to `''`. `valueAsNumber` is milliseconds since midnight (`NaN` while blank),
  and `valueAsDate` applies the clock fields to today's local date (`null` while blank). Both are
  settable, like the native `<input type="time">` properties they mirror: assigning `valueAsNumber`
  sets `value` from the same scale, and out-of-range or non-finite figures clear the field rather
  than wrapping into a different time; assigning `valueAsDate` reads the same local clock fields
  back off the Date, so it round-trips with the getter, and `null`/an invalid Date clears. Both
  assignments are silent, again like the native properties.
- `defaultValue`, `name`, `form`, `disabled`, `required`, `customError`, `getForm()`,
  `checkValidity()`, `reportValidity()`, `setCustomValidity()`, and `resetValidity()` use the shared form-control
  contract. Reset restores the current declarative `value` default; `readonly` remains focusable
  and submits but cannot be edited. `resetValidity()` clears only consumer custom validity and
  restores the current intrinsic time constraints; it leaves the value/default and interaction
  state unchanged.
- `min = ''`, `max = ''` accept the same canonical time syntax. `min <= max` is an ordinary
  closed range; `min > max` is an overnight range (for example `22:00` through `06:00`).
- `step: number | 'any' = 60` is seconds. A numeric value below 60 reveals seconds. Numeric
  validation follows native time step-base precedence: valid `min`, otherwise the current reset
  default (`defaultValue`/the `value` content attribute), otherwise midnight. Picker options are
  projected from the complete valid-time grid, so offset, hourly, multi-hour, bounded and overnight
  grids expose only reachable values and retain a selected valid value. `'any'` disables step
  mismatch and exposes the unrestricted segment vocabulary.
- `hourFormat: 'auto' | '12' | '24' = 'auto'` (`hour-format`) overrides the locale's hour cycle.
- `appearance: 'accent' | 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'outlined'`,
  `size` (shared `2xs`…`xl` ladder and `small`/`medium`/`large` aliases), and `pill = false`.
- `label = ''`, `hint = ''`, `errorText = ''` (`error-text`), `withLabel = false`
  (`with-label`), and `withHint = false` (`with-hint`) provide complete form chrome. A host
  `aria-label` wins for the internal editing surface's accessible name.
- `open = false`, `placement = 'bottom-start'`, and `distance = 0` control the picker.
  `show()` / `hide()` return `Promise<void>` and settle after the matching `lr-after-*` event.
- `withClear = false` (`with-clear`) adds a localized clear action. `withNow = false`
  (`with-now`) adds a localized Now footer unless the `footer` slot replaces it.
- `autocomplete = ''` is forwarded to a visually hidden, nameless native time input used only as
  the browser autofill seam; the FACE host remains the sole submitted control.

**Methods:** `focus(options?)`, `blur()`, and `click()` delegate to the active segment; `focus()` and
`click()` are synchronous no-ops while directly or fieldset disabled, so they cannot create focus
events from a removed tab stop. If a controlled locale, `hourFormat`, or `step` change removes the
segment that currently owns focus, the first surviving segment receives focus after the new pattern
renders. A format change never reclaims focus from another control. `show()` and `hide()` control the
picker, while its form methods are described above. Disconnecting force-closes without a veto and
reconnects with `open`, its attribute, ARIA, popup visibility, and `:state(open)` all closed. A
visible incomplete draft retains its segments, digit buffer, empty submitted value, and `badInput`
state across detach/adoption/reconnect.

**Keyboard:** only one segment is in the tab order. Digits fill the active segment and advance when
no further digit can be accepted; Left/Right moves in locale order and reverses under RTL;
Up/Down steps, Home/End selects the segment edge, and Backspace/Delete clears the segment.
Pasting a canonical time replaces the full value as one edit. Alt+ArrowDown opens the picker.
Inside a picker column, one enabled option is tabbable; ArrowUp/ArrowDown rove, Home/End jump to
the bounds, and Enter/Space activate the focused native option button. Disabled controls project
`disabled` and `tabindex=-1` to every picker option.
`readonly` keeps navigation and popup browsing but blocks commits; `disabled` removes the tab stop,
popup, validation, and form submission.

**Events:** native `input` on user edits and native `change` on a complete commit; compatibility
aliases `lr-input` / `lr-change` carry `{ value }`. `focus` / `blur` cross the shadow boundary once.
`lr-clear` follows a clear. Cancelable `lr-show` / `lr-hide`
precede popup state changes; `lr-after-show` / `lr-after-hide` follow motion settlement.
`lr-invalid` follows a failed validity check.
The hidden native autofill seam treats an intentional empty `input` followed by `change` as a
single clear transaction, emitting `input`, `lr-input`, `change`, then `lr-change` exactly once.

**Slots:** `label`, `hint`, `error`, `start`, `end`, `clear-icon`, `expand-icon`, and `footer`.

**CSS parts:** `form-control`, `form-control-input`, `form-control-label label` (same node),
`base time-input input-wrapper` (same node), `input`, `segment`, `segment-literal`, `start`, `end`,
`clear-button`, `expand-button`, `expand-icon`, `popup`, `columns`, `column`, `column-item`,
`column-item-selected`, `now-button`, `hint`, and `error`.
Each `column` is a block-axis scroll container and explicitly clips inline overflow, so an
undersized `--column-width` cannot introduce a second scrollbar.
The label/hint/error chrome wraps unbroken localized content within the host, while `start`/`end`
adornments shrink and ellipsize. The exact-320px RTL story keeps that copy, the seconds segments,
fixed-size actions, and the open picker contained.

`error` is ordinary visible validation text referenced by the segmented input through
`aria-describedby`, not a shadow `role="alert"`. Native `reportValidity()`/focus feedback therefore
has one description path instead of being duplicated by a second live-region announcement.
The group and every spinbutton expose explicit stateful `aria-invalid`: visible property/slotted
error chrome makes it `"true"` immediately, as does intrinsic/custom invalidity after interaction;
otherwise each owner explicitly exposes `"false"`.
Every spinbutton renders explicit `aria-required="true"` or `"false"`. While required, the
segmented `role="group"` also acquires a localized visually-hidden requiredness description without
overwriting its existing hint/error relationship; removing `required` releases only that text.

**Custom states:** `blank`, `disabled`, and `open`, plus the shared validity states.

**The required marker.** `required` with a non-empty `label` paints the library's shared marker on
the `form-control-label` node — the one `::after` rule described under "The required-field marker"
above, 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`. With no label text the part is hidden and no glyph is
painted.

**Themeable custom properties:** `--lr-time-input-gap` (outer segment/adornment/action gap,
default `--lr-form-control-gap`) and `--lr-time-input-radius` (outer row radius, default
`--lr-form-control-radius`, or `--lr-radius-pill` with `pill`) remain undeclared on the host, so an
ancestor theme wrapper or direct-host value overrides those fallbacks. Also available are
`--lr-time-input-border-color`, `--lr-time-input-fill`, and `--lr-time-input-color` for the
appearance surface; `--lr-time-input-focus-border-color`;
`--lr-time-input-segment-hover-bg`, `--lr-time-input-segment-active-bg`, and
`--lr-time-input-segment-focus-bg`; `--lr-time-input-action-color`,
`--lr-time-input-action-hover-color`, `--lr-time-input-action-hover-bg`, and
`--lr-time-input-action-active-bg`; and `--lr-time-input-column-hover-bg`,
`--lr-time-input-column-active-bg`, `--lr-time-input-column-selected-bg`,
`--lr-time-input-column-selected-color`, `--lr-time-input-column-selected-font-weight`,
`--lr-time-input-column-selected-hover-bg`, and `--lr-time-input-column-selected-active-bg`.
Outer row height is a pair, not one knob: `--lr-time-input-control-min-height` (default
`var(--lr-form-control-height)`) is the row's height **floor**, taken from the active `size` tier of
the shared form-control ladder, so a time field is exactly as tall as an `lr-input` or `lr-select`
at the same tier; `--lr-time-input-control-height` is **unset by default** and, given a length,
pins the row to exactly that height — flooring and capping it at once, which is what pixel-matches
an `lr-date-input` sharing the same toolbar. Leaving `--lr-time-input-control-height` unset keeps
`--lr-time-input-control-min-height` working as a floor only, and because the component never
declares `--lr-time-input-control-height` on its own host it can be set from an ancestor or an
outer-tree rule just as well as inline on the element.
Every state hook falls back to the exact semantic token or color mix used previously, and remains
undeclared on the host so ancestor themes work. The upstream-compatible `--column-item-height`
(default `calc(var(--lr-size-1em) * 2.25)`) and `--column-width`
(default `calc(var(--lr-size-1em) * 3)`) retain their component-font-relative sizing through the
Lyra `--lr-size-1em` token. `--show-duration` and `--hide-duration` similarly use Lyra
duration-token fallbacks. Each `column` also honors the opt-in theme-level
`--lr-theme-scrollbar-width`/`--lr-theme-scrollbar-gutter` hooks (defaults `thin`/`auto`, matching
its previous unconditional `scrollbar-width: thin`) — set either on `:root` or any ancestor for one
declaration to retheme every internal scroll container in the library.

The shared field halo `--lr-form-control-focus-shadow` (default `none`) paints a `box-shadow` while
this control is focused or open — one name for every field-shaped control in the library, so a halo
is configured once instead of per component. It is additive: the focus outline and the
`--lr-time-input-focus-border-color` edge are the accessibility answer to focus and are never
replaced by it.

The `popup` panel is a floating surface and paints from the **shared overlay-surface family**
(16.0.0): `--lr-overlay-surface`, `--lr-overlay-border` (default `var(--lr-color-border)`),
`--lr-overlay-radius` (default `var(--lr-radius)`) and `--lr-overlay-shadow-anchored` (default
`var(--lr-shadow-m)`). None is declared on `:host`, so one declaration on `:root` — or on any
ancestor, to scope it — retints this panel together with every other floating surface in the
library. One deliberate difference from the rest of the family: `--lr-overlay-surface` defaults here
to `var(--lr-color-surface-raised)` rather than the family's own
`var(--lr-color-surface-overlay)`. The picker is a dense grid of time cells dropped from a field,
and the raised tone is what separates it from the field's fill — `--lr-color-surface-overlay`
resolves to the plain page surface in light mode and would erase that separation, while in dark
mode the raised tone is already distinct from the page, so this panel never had the
reads-as-a-hole problem the family exists to fix. Setting `--lr-overlay-surface` still repaints it
along with every other popup.

`--lr-positioning-strategy` (16.0.0) — the `popup` panel reads this same cascading
`absolute`/`fixed` override documented on `<lr-popover>` when it is (re)positioned, falling back to
its own `fixed` default when nothing is set. There is no per-instance `positioning-strategy`
property on `<lr-time-input>`; set the custom property on `:root`, a theme, or one clipping
ancestor to change every unset time input beneath it.

```html
<lr-time-input
  label="Start time"
  value="09:30"
  with-clear
  with-now
></lr-time-input>
<lr-time-input label="Precise time" step="15" value="09:30:15"></lr-time-input>
<script type="module">
  import "@aceshooting/lyra-ui/components/forms/input/time-input.js";
</script>
```
