{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"number-input-clean","type":"registry:ui","title":"Number Input (Clean)","description":"A field for one number – − and + that repeat while held, arrow keys and Page Up / Down, min, max and step, a unit like tokens or %, formatted for the locale","dependencies":[],"files":[{"path":"registry/number-input-clean/number-input-clean.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState, type ComponentPropsWithRef, type ReactNode } from \"react\";\n\n/*\n * Number Input – a single self-contained file, clean style.\n * A field for one number: − and + to step it (hold to keep going), the arrow keys, Page Up / Page Down for big steps,\n * Home / End for the limits. A unit sits right after the value (\"4,000 tokens\", \"25 %\"), the value is formatted for\n * the locale when you leave the field and kept between min and max. Label, hint and error like the Text Field; focus\n * only firms up the edge – the buttons get the ring. The styles are injected below, so no extra files are needed.\n */\n\nexport type NumberInputProps = Omit<\n  ComponentPropsWithRef<\"input\">,\n  \"type\" | \"size\" | \"prefix\" | \"value\" | \"defaultValue\" | \"onChange\" | \"min\" | \"max\" | \"step\"\n> & {\n  /** The number; null is an empty field. Pass it with onValueChange to control the field */\n  value?: number | null;\n  /** The starting number when the field keeps its own value */\n  defaultValue?: number | null;\n  /** Called with the new number after a step, or when typing is done (blur or Enter) – null when emptied */\n  onValueChange?: (value: number | null) => void;\n  /** The lowest value – typing less is raised to it, − stops there */\n  min?: number;\n  /** The highest value – typing more is lowered to it, + stops there */\n  max?: number;\n  /** How much − / + and the arrow keys change the value */\n  step?: number;\n  /** How much Page Up / Page Down change it – 10 steps by default */\n  largeStep?: number;\n  /** Text after the value – e.g. \"tokens\" or \"%\" */\n  unit?: ReactNode;\n  /** Text or an icon before the value – e.g. \"$\" */\n  prefix?: ReactNode;\n  /** The locale the value is shown and read in – the browser's by default */\n  locale?: string;\n  /** How the value is shown, e.g. { maximumFractionDigits: 2 } or { style: \"percent\" } */\n  formatOptions?: Intl.NumberFormatOptions;\n  /** Shown above the field and tied to it */\n  label?: ReactNode;\n  /** A quiet line under the field – the error takes its place while there is one */\n  hint?: ReactNode;\n  /** Marks the field invalid and shows this under it, in place of the hint */\n  error?: ReactNode;\n  /** \"Optional\" next to the label */\n  optional?: boolean;\n  /** sm 28 · md 32 · lg 36 high */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Extra class on the input itself; className goes on the wrapper */\n  inputClassName?: string;\n};\n\nexport function NumberInput({\n  value,\n  defaultValue = null,\n  onValueChange,\n  min,\n  max,\n  step = 1,\n  largeStep,\n  unit,\n  prefix,\n  locale,\n  formatOptions,\n  label,\n  hint,\n  error,\n  optional = false,\n  size = \"md\",\n  className,\n  inputClassName,\n  id,\n  name,\n  form,\n  disabled,\n  readOnly,\n  placeholder,\n  onKeyDown,\n  onBlur,\n  ref,\n  \"aria-describedby\": describedBy,\n  ...rest\n}: NumberInputProps) {\n  const autoId = useId();\n  const inputId = id ?? `${autoId}-input`;\n  const messageId = `${autoId}-message`;\n  const input = useRef<HTMLInputElement | null>(null);\n\n  const [own, setOwn] = useState<number | null>(defaultValue);\n  const controlled = value !== undefined;\n  const current = controlled ? value : own;\n  const num = current != null && Number.isFinite(current) ? current : null;\n  // What's typed while the field is being edited; null shows the formatted value\n  const [draft, setDraft] = useState<string | null>(null);\n\n  const stepSize = step > 0 && Number.isFinite(step) ? step : 1;\n  const bigStep = largeStep && largeStep > 0 ? largeStep : stepSize * 10;\n  const lo = min ?? -Infinity;\n  const hi = max ?? Infinity;\n\n  const fmt = useMemo(() => {\n    try {\n      return new Intl.NumberFormat(locale, formatOptions);\n    } catch {\n      return new Intl.NumberFormat(undefined, formatOptions);\n    }\n  }, [locale, formatOptions]);\n  const percent = formatOptions?.style === \"percent\";\n  const marks = useMemo(() => separators(fmt), [fmt]);\n\n  const shown = draft ?? (num === null ? \"\" : fmt.format(num));\n  const invalid = Boolean(error) || rest[\"aria-invalid\"] === true || rest[\"aria-invalid\"] === \"true\";\n  const message = shows(error) ? error : shows(hint) ? hint : null;\n  const scale = SIZES.includes(size) ? size : \"md\";\n  const fallbackName = label == null && !rest[\"aria-label\"] && !rest[\"aria-labelledby\"] && typeof placeholder === \"string\" ? placeholder : undefined;\n\n  const clamp = (v: number) => Math.min(hi, Math.max(lo, v));\n  const set = (next: number | null) => {\n    if (!controlled) setOwn(next);\n    if (next !== num) onValueChange?.(next);\n  };\n\n  /** Reads what's typed: the locale's separators, a minus, a percent – null when empty, NaN when it's no number */\n  const read = (text: string): number | null => {\n    let t = text.replace(/[\\s  ]/g, \"\");\n    if (t === \"\") return null;\n    if (marks.group) t = t.split(marks.group).join(\"\");\n    if (marks.decimal !== \".\") t = t.split(marks.decimal).join(\".\");\n    const negative = /^[-−]|[-−]$|^\\(.*\\)$/.test(t);\n    t = t.replace(/[^\\d.]/g, \"\");\n    if (t === \"\" || t === \".\" || (t.match(/\\./g)?.length ?? 0) > 1) return NaN;\n    const n = Number(t) * (negative ? -1 : 1);\n    return percent ? n / 100 : n;\n  };\n\n  /** Takes what's typed: clamped, formatted; something unreadable goes back to the last value */\n  const commit = () => {\n    if (draft === null) return;\n    const parsed = read(draft);\n    setDraft(null);\n    if (parsed === null) return set(null);\n    if (!Number.isFinite(parsed)) return;\n    set(clamp(round(parsed, stepSize)));\n  };\n\n  /** One step up or down from the current (or typed) value, onto the step grid */\n  const nudge = (dir: 1 | -1, by = stepSize) => {\n    if (disabled || readOnly) return;\n    const typed = draft !== null ? read(draft) : num;\n    const base = typed !== null && Number.isFinite(typed) ? typed : null;\n    // An empty field starts at min (or 0) – the first press doesn't skip a step\n    const origin = Number.isFinite(lo) ? lo : 0;\n    let next: number;\n    if (base === null) next = clamp(Number.isFinite(lo) ? lo : 0);\n    else if (by !== stepSize) next = clamp(round(origin + Math.round((base + dir * by - origin) / stepSize) * stepSize, stepSize));\n    else {\n      const k = (base - origin) / by;\n      // Off the grid: the first step lands on it instead of jumping past\n      const snapped = dir > 0 ? Math.floor(k + 1e-9) + 1 : Math.ceil(k - 1e-9) - 1;\n      next = clamp(round(origin + snapped * by, by));\n    }\n    setDraft(null);\n    // Already at the limit: nothing moves\n    if (next === base) {\n      if (draft !== null) set(next);\n      return;\n    }\n    set(next);\n    roll(input.current, dir);\n  };\n\n  const atMin = num !== null && num <= lo;\n  const atMax = num !== null && num >= hi;\n\n  // Hold − or + to keep stepping: a pause, then faster\n  const hold = useRef<{ timer: number; dir: 1 | -1 } | null>(null);\n  const stop = () => {\n    if (hold.current) window.clearTimeout(hold.current.timer);\n    hold.current = null;\n  };\n  const nudgeRef = useRef(nudge);\n  nudgeRef.current = nudge;\n  const press = (dir: 1 | -1) => {\n    stop();\n    nudgeRef.current(dir);\n    const tick = (delay: number) => {\n      hold.current = {\n        dir,\n        timer: window.setTimeout(() => {\n          nudgeRef.current(dir);\n          // After the first pause it repeats quickly and speeds up a little\n          tick(delay >= 400 ? 120 : Math.max(40, delay * 0.9));\n        }, delay),\n      };\n    };\n    tick(400);\n  };\n  useEffect(() => stop, []);\n  // Stops at the limit, so a held button doesn't keep firing at nothing\n  useEffect(() => {\n    if (hold.current && ((hold.current.dir > 0 && atMax) || (hold.current.dir < 0 && atMin))) stop();\n  }, [atMax, atMin]);\n\n  const unitText = typeof unit === \"string\" ? unit : \"\";\n  const valueText = num === null ? undefined : `${typeof prefix === \"string\" ? prefix : \"\"}${fmt.format(num)}${unitText ? ` ${unitText}` : \"\"}`;\n\n  const stepper = (dir: 1 | -1) => {\n    const blocked = disabled || readOnly || (dir > 0 ? atMax : atMin);\n    return (\n      <button\n        type=\"button\"\n        className=\"ui-nmic__step\"\n        // One tab stop per field, as in the spinbutton pattern: the arrow keys step, the buttons are for the pointer\n        tabIndex={-1}\n        aria-label={dir > 0 ? \"Increase\" : \"Decrease\"}\n        aria-controls={inputId}\n        aria-disabled={blocked || undefined}\n        disabled={disabled}\n        // Keeps the caret in the field while you click, and starts the hold-to-repeat\n        onPointerDown={(e) => {\n          if (e.button !== 0) return;\n          e.preventDefault();\n          if (!blocked) press(dir);\n        }}\n        onPointerUp={stop}\n        onPointerLeave={stop}\n        onPointerCancel={stop}\n        onContextMenu={(e) => e.preventDefault()}\n        // Enter or Space on a focused button (pointer clicks are handled above)\n        onClick={(e) => {\n          if (e.detail === 0 && !blocked) nudge(dir);\n        }}\n      >\n        {dir > 0 ? <PlusIcon /> : <MinusIcon />}\n      </button>\n    );\n  };\n\n  return (\n    <div\n      className={[\"ui-nmic\", className].filter(Boolean).join(\" \")}\n      data-size={scale}\n      data-invalid={invalid || undefined}\n      data-disabled={disabled || undefined}\n      data-readonly={readOnly || undefined}\n    >\n      {/* React 19 hoists this into <head> once, no matter how many fields render */}\n      <style href=\"ui-number-input-clean\" precedence=\"default\">\n        {css}\n      </style>\n      {label != null && (\n        <div className=\"ui-nmic__head\">\n          <label className=\"ui-nmic__label\" htmlFor={inputId}>\n            {label}\n          </label>\n          {optional && <span className=\"ui-nmic__optional\">Optional</span>}\n        </div>\n      )}\n      {/* A click anywhere on the box – the unit, the padding – puts the caret in the input */}\n      <div\n        className=\"ui-nmic__box\"\n        onPointerDown={(e) => {\n          if (e.target === input.current || (e.target as HTMLElement).closest(\"button\")) return;\n          e.preventDefault();\n          input.current?.focus();\n        }}\n      >\n        {prefix != null && <span className=\"ui-nmic__affix\" data-side=\"start\">{prefix}</span>}\n        <input\n          {...rest}\n          ref={(el) => {\n            input.current = el;\n            if (typeof ref === \"function\") ref(el);\n            else if (ref) ref.current = el;\n          }}\n          id={inputId}\n          className={[\"ui-nmic__input\", inputClassName].filter(Boolean).join(\" \")}\n          type=\"text\"\n          // Phone keypads: digits only, a decimal key when the value has decimals, the full keyboard when it can go below 0\n          inputMode={lo < 0 ? \"text\" : !Number.isInteger(stepSize) || marks.decimals || percent ? \"decimal\" : \"numeric\"}\n          role=\"spinbutton\"\n          autoComplete=\"off\"\n          autoCorrect=\"off\"\n          spellCheck={false}\n          value={shown}\n          placeholder={placeholder}\n          disabled={disabled}\n          readOnly={readOnly}\n          form={form}\n          aria-label={rest[\"aria-label\"] ?? fallbackName}\n          aria-valuenow={num ?? undefined}\n          aria-valuemin={Number.isFinite(lo) ? lo : undefined}\n          aria-valuemax={Number.isFinite(hi) ? hi : undefined}\n          aria-valuetext={valueText}\n          aria-invalid={invalid || undefined}\n          aria-describedby={[describedBy, message ? messageId : null].filter(Boolean).join(\" \") || undefined}\n          // The value hugs its text, so the unit sits right after it\n          style={{ width: `calc(${Math.max(2, (shown || placeholder || \"\").length)}ch + 2px)`, ...rest.style }}\n          onChange={(e) => setDraft(e.target.value.replace(ALLOWED, \"\"))}\n          onBlur={(e) => {\n            commit();\n            onBlur?.(e);\n          }}\n          onKeyDown={(e) => {\n            onKeyDown?.(e);\n            if (e.defaultPrevented || e.nativeEvent.isComposing) return;\n            const keys: Record<string, () => void> = {\n              ArrowUp: () => nudge(1),\n              ArrowDown: () => nudge(-1),\n              PageUp: () => nudge(1, bigStep),\n              PageDown: () => nudge(-1, bigStep),\n              Enter: () => commit(),\n              Escape: () => setDraft(null),\n            };\n            if (Number.isFinite(lo)) keys.Home = () => (setDraft(null), set(lo), roll(input.current, -1));\n            if (Number.isFinite(hi)) keys.End = () => (setDraft(null), set(hi), roll(input.current, 1));\n            const run = keys[e.key];\n            if (!run || readOnly) return;\n            // Enter still submits the form; Escape only counts while something was typed\n            if (e.key === \"Escape\" && draft === null) return;\n            if (e.key !== \"Enter\") e.preventDefault();\n            run();\n          }}\n        />\n        {unit != null && <span className=\"ui-nmic__affix\" data-side=\"end\">{unit}</span>}\n        <span className=\"ui-nmic__steps\">\n          {stepper(-1)}\n          {stepper(1)}\n        </span>\n      </div>\n      {/* The plain number goes with the form, not the formatted text */}\n      {name && <input type=\"hidden\" name={name} form={form} value={num ?? \"\"} disabled={disabled} />}\n      {message && (\n        <p className=\"ui-nmic__message\" id={messageId} key={shows(error) ? \"error\" : \"hint\"} dir=\"auto\" aria-live={shows(error) ? \"polite\" : undefined}>\n          {message}\n        </p>\n      )}\n    </div>\n  );\n}\n\nconst SIZES = [\"sm\", \"md\", \"lg\"];\n/** What can be typed: digits, separators, a minus, a percent sign – letters never show up */\nconst ALLOWED = /[^\\d.,'’\\s  \\-−%]/g;\n/** Text or an element – not true, false, null or an empty string */\nconst shows = (v: ReactNode) => v != null && typeof v !== \"boolean\" && v !== \"\";\n\n/** The locale's group and decimal marks, and whether the format always shows decimals */\nfunction separators(fmt: Intl.NumberFormat) {\n  const parts = fmt.formatToParts(-12345.6789);\n  return {\n    group: parts.find((p) => p.type === \"group\")?.value ?? \"\",\n    decimal: parts.find((p) => p.type === \"decimal\")?.value ?? \".\",\n    // Shown with decimals on purpose (minimumFractionDigits), so a decimal key is needed\n    decimals: (fmt.resolvedOptions().minimumFractionDigits ?? 0) > 0,\n  };\n}\n\n/** Rounds away float noise (0.1 + 0.2) to the step's own decimals, at least 10 */\nfunction round(v: number, step: number) {\n  const places = Math.max(10, (String(step).split(\".\")[1] ?? \"\").length);\n  return Number(v.toFixed(Math.min(places, 15)));\n}\n\n/** The one motion: the new value rolls in from the direction it moved */\nfunction roll(el: HTMLInputElement | null, dir: 1 | -1) {\n  if (!el || typeof el.animate !== \"function\") return;\n  if (window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches) return;\n  el.animate(\n    [\n      { transform: `translateY(${dir * 5}px)`, opacity: 0.35, filter: \"blur(1.5px)\" },\n      { transform: \"none\", opacity: 1, filter: \"none\" },\n    ],\n    { duration: 220, easing: \"cubic-bezier(0.22, 1, 0.36, 1)\" },\n  );\n}\n\n/* Icons: Hugeicons Free (MIT), drawn at 16px */\nfunction MinusIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" aria-hidden=\"true\">\n      <path d=\"M20 12H4\" />\n    </svg>\n  );\n}\nfunction PlusIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" aria-hidden=\"true\">\n      <path d=\"M12 4v16M20 12H4\" />\n    </svg>\n  );\n}\n\nconst css = /* css */ `\n/* Colors and motion as variables – override them on .ui-nmic (see Theming in the docs) */\n.ui-nmic {\n  --ui-nmic-title: var(--aui-ink, rgba(0, 0, 0, 0.88));\n  --ui-nmic-ink: var(--aui-ink, rgba(0, 0, 0, 0.88));\n  --ui-nmic-subtle: var(--aui-muted, rgba(0, 0, 0, 0.45));\n  --ui-nmic-faint: var(--aui-placeholder, rgba(0, 0, 0, 0.3));\n  --ui-nmic-icon: var(--aui-muted, rgba(0, 0, 0, 0.447));\n  --ui-nmic-surface: var(--aui-surface, #ffffff);\n  --ui-nmic-wash: rgba(0, 0, 0, 0.04);\n  --ui-nmic-press: rgba(0, 0, 0, 0.07);\n  --ui-nmic-readonly: rgba(0, 0, 0, 0.02);\n  --ui-nmic-shadow: 0 0 0 0.5px var(--aui-border, rgba(0, 0, 0, 0.08)), 0 1px 1px -1px rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n  --ui-nmic-shadow-hover: 0 0 0 0.5px var(--aui-border-strong, rgba(0, 0, 0, 0.13)), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 1px 2px 0 rgba(0, 0, 0, 0.06);\n  --ui-nmic-shadow-focus: 0 0 0 1px var(--aui-border-strong, rgba(0, 0, 0, 0.28)), 0 1px 2px 0 rgba(0, 0, 0, 0.06);\n  --ui-nmic-danger: var(--aui-danger, #c4320a);\n  --ui-nmic-danger-edge: color-mix(in srgb, var(--aui-red, #f04438) 55%, transparent);\n  --ui-nmic-danger-edge-focus: rgba(229, 72, 77, 0.8);\n  --ui-nmic-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  --ui-nmic-ease: cubic-bezier(0.22, 1, 0.36, 1);\n\n  --h: 32px;\n  --px: 10px;\n  --fs: 13px;\n  --radius: var(--aui-radius, 7px);\n\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  min-width: 0;\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 13px;\n  line-height: 20px;\n  color: var(--ui-nmic-ink);\n  -webkit-font-smoothing: antialiased;\n}\n.ui-nmic[data-size=\"sm\"] { --h: 28px; --px: 8px; --radius: calc(var(--aui-radius, 7px) - 1px); }\n.ui-nmic[data-size=\"lg\"] { --h: 36px; --px: 12px; --fs: 14px; --radius: calc(var(--aui-radius, 7px) + 1px); }\n/* Touch screens: a comfortable tap height, and 16px text so iOS doesn't zoom in on focus */\n@media (pointer: coarse) {\n  .ui-nmic { --fs: 16px; }\n  .ui-nmic[data-size=\"sm\"] { --h: 36px; }\n  .ui-nmic[data-size=\"md\"] { --h: 40px; }\n  .ui-nmic[data-size=\"lg\"] { --h: 44px; }\n}\n\n.ui-nmic__head { display: flex; align-items: baseline; gap: 6px; min-width: 0; }\n.ui-nmic__label { min-width: 0; font-weight: 500; color: var(--ui-nmic-title); overflow-wrap: anywhere; cursor: default; }\n.ui-nmic__optional { flex: none; font-size: 12px; line-height: 16px; color: var(--ui-nmic-subtle); }\n\n/* The box: white, a hairline shadow as its edge; hover and focus only make that edge firmer – no ring */\n.ui-nmic__box {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  box-sizing: border-box;\n  height: var(--h);\n  min-width: 0;\n  padding-inline: var(--px) 4px;\n  border-radius: var(--radius);\n  background: var(--ui-nmic-surface);\n  box-shadow: var(--ui-nmic-shadow);\n  cursor: text;\n  transition: box-shadow 140ms ease, background-color 140ms ease;\n}\n.ui-nmic__box:hover { box-shadow: var(--ui-nmic-shadow-hover); }\n.ui-nmic__box:has(.ui-nmic__input:focus) { box-shadow: var(--ui-nmic-shadow-focus); }\n.ui-nmic[data-invalid] .ui-nmic__box { box-shadow: 0 0 0 1px var(--ui-nmic-danger-edge), 0 1px 2px 0 rgba(0, 0, 0, 0.05); }\n.ui-nmic[data-invalid] .ui-nmic__box:has(.ui-nmic__input:focus) { box-shadow: 0 0 0 1px var(--ui-nmic-danger-edge-focus), 0 1px 2px 0 rgba(0, 0, 0, 0.05); }\n.ui-nmic[data-readonly] .ui-nmic__box { background: var(--ui-nmic-readonly); box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.08); cursor: default; }\n.ui-nmic[data-disabled] .ui-nmic__box { background: var(--ui-nmic-readonly); box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.06); cursor: not-allowed; }\n.ui-nmic[data-disabled] { opacity: 0.55; }\n\n/* The value: as wide as its text (set inline), so the unit follows it; it shrinks before the buttons do */\n.ui-nmic__input {\n  flex: 0 1 auto;\n  min-width: 2ch;\n  max-width: 100%;\n  height: 100%;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  outline: 0;\n  background: transparent;\n  font: inherit;\n  font-size: var(--fs);\n  font-variant-numeric: tabular-nums;\n  color: var(--ui-nmic-ink);\n  text-overflow: ellipsis;\n  -webkit-appearance: none;\n  appearance: none;\n}\n.ui-nmic__input::placeholder { color: var(--ui-nmic-faint); opacity: 1; }\n.ui-nmic__input:disabled { cursor: not-allowed; }\n\n/* Prefix and unit: quiet text right against the value */\n.ui-nmic__affix {\n  display: inline-flex;\n  flex: 0 1 auto;\n  align-items: center;\n  min-width: 0;\n  max-width: 40%;\n  font-size: var(--fs);\n  color: var(--ui-nmic-subtle);\n  white-space: nowrap;\n  user-select: none;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n.ui-nmic__affix[data-side=\"start\"] { margin-inline-end: -2px; }\n.ui-nmic__affix > svg { display: block; width: 16px; height: 16px; flex: none; color: var(--ui-nmic-icon); }\n\n/* − and +: two small ghost buttons at the end */\n.ui-nmic__steps { display: flex; flex: none; gap: 2px; margin-inline-start: auto; padding-inline-start: 4px; }\n.ui-nmic__step {\n  display: grid;\n  place-items: center;\n  position: relative;\n  width: calc(var(--h) - 8px);\n  height: calc(var(--h) - 8px);\n  max-width: 26px;\n  max-height: 26px;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  border-radius: calc(var(--radius) - 3px);\n  background: transparent;\n  color: var(--ui-nmic-icon);\n  cursor: pointer;\n  touch-action: manipulation;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-tap-highlight-color: transparent;\n  transition: background-color 140ms ease, color 140ms ease;\n}\n.ui-nmic__step:hover:not([aria-disabled]):not(:disabled) { background: var(--ui-nmic-wash); color: var(--ui-nmic-ink); }\n.ui-nmic__step:active:not([aria-disabled]):not(:disabled) { background: var(--ui-nmic-press); }\n.ui-nmic__step:focus-visible { outline: 2px solid var(--ui-nmic-focus); outline-offset: -2px; }\n.ui-nmic__step[aria-disabled], .ui-nmic__step:disabled { opacity: 0.35; cursor: default; }\n.ui-nmic__step svg { display: block; transition: transform 120ms var(--ui-nmic-ease); }\n.ui-nmic__step:active:not([aria-disabled]):not(:disabled) svg { transform: scale(0.86); }\n.ui-nmic[data-readonly] .ui-nmic__steps { display: none; }\n/* Touch screens: bigger buttons and a hit area that reaches past them */\n@media (pointer: coarse) {\n  .ui-nmic__step { width: 32px; height: 32px; max-width: none; max-height: none; }\n  .ui-nmic__step::before { content: \"\"; position: absolute; inset: -4px 0; }\n}\n\n/* Under the field: the hint, or the error in its place */\n.ui-nmic__message {\n  margin: 0;\n  font-size: 12px;\n  line-height: 16px;\n  color: var(--ui-nmic-subtle);\n  overflow-wrap: anywhere;\n  animation: ui-nmic-message 260ms var(--ui-nmic-ease);\n}\n.ui-nmic[data-invalid] .ui-nmic__message { color: var(--ui-nmic-danger); }\n\n@keyframes ui-nmic-message {\n  from { opacity: 0; transform: translateY(-3px); filter: blur(2px); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ui-nmic * { animation-duration: 1ms !important; transition-duration: 1ms !important; }\n  .ui-nmic__box { transition-duration: 1ms; }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-nmic {\n  --ui-nmic-title: var(--aui-ink, rgba(255, 255, 255, 0.92));\n  --ui-nmic-ink: var(--aui-ink, rgba(255, 255, 255, 0.9));\n  --ui-nmic-subtle: var(--aui-muted, rgba(255, 255, 255, 0.46));\n  --ui-nmic-faint: var(--aui-placeholder, rgba(255, 255, 255, 0.3));\n  --ui-nmic-icon: var(--aui-muted, rgba(255, 255, 255, 0.5));\n  --ui-nmic-surface: var(--aui-surface, #1c1c1e);\n  --ui-nmic-wash: rgba(255, 255, 255, 0.07);\n  --ui-nmic-press: rgba(255, 255, 255, 0.11);\n  --ui-nmic-readonly: rgba(255, 255, 255, 0.03);\n  --ui-nmic-shadow: 0 0 0 0.5px var(--aui-border, rgba(255, 255, 255, 0.14)), 0 1px 2px 0 rgba(0, 0, 0, 0.4);\n  --ui-nmic-shadow-hover: 0 0 0 0.5px var(--aui-border-strong, rgba(255, 255, 255, 0.2)), 0 1px 2px 0 rgba(0, 0, 0, 0.4);\n  --ui-nmic-shadow-focus: 0 0 0 1px var(--aui-border-strong, rgba(255, 255, 255, 0.3)), 0 1px 2px 0 rgba(0, 0, 0, 0.4);\n  --ui-nmic-danger: var(--aui-danger, #ff8a7a);\n  --ui-nmic-danger-edge: color-mix(in srgb, var(--aui-red, #ff6b5f) 50%, transparent);\n  --ui-nmic-danger-edge-focus: rgba(255, 99, 105, 0.75);\n  --ui-nmic-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-nmic[data-readonly] .ui-nmic__box,\n:where(.dark, [data-theme=\"dark\"]) .ui-nmic[data-disabled] .ui-nmic__box { box-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.12); }\n`;\n"}]}