{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "type": "registry:block",
  "title": "Combobox",
  "description": "A searchable select built on a real <select>: type to filter grouped options with the matched letters highlighted, while a pill springs between rows and the popover springs open from the field. Full ARIA combobox keyboard support, a clear button and optional multi-select chips.",
  "docs": "Combobox was added to public/vividsites/combobox.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/combobox",
  "categories": [
    "ui",
    "forms"
  ],
  "files": [
    {
      "path": "components/vividsites/combobox.html",
      "type": "registry:file",
      "target": "public/vividsites/combobox.html",
      "content": "<!--\nvs-combobox: a searchable select. It starts life as a real <select> (so it works with no\n  JS and submits with the form), then grows into a WAI-ARIA combobox: an input with\n  role=\"combobox\" driving a grouped role=\"listbox\" through aria-activedescendant.\n  Type to filter (accent-insensitive, prefix > word-start > substring > fuzzy, matched\n  letters highlighted), a highlight pill that springs between rows and stretches with its\n  speed, a popover that springs open from the field and can be reversed mid-flight, a\n  list height that springs as results change, drawn check marks, a clear button and,\n  with `multiple`, removable chips.\n\n  Markup:\n    <div class=\"vs-combobox\" data-label=\"Home time zone\" data-recent=\"Europe/Lisbon,Asia/Tokyo\">\n      <select name=\"tz\">                      (add `multiple` for chips)\n        <optgroup label=\"Europe\">\n          <option value=\"Europe/Lisbon\" data-sub=\"Portugal · LIS\" data-keywords=\"lisboa\" data-tz=\"Europe/Lisbon\" selected>Lisbon</option>\n        </optgroup>\n      </select>\n    </div>\n  Root attributes:\n    data-label        visible label text (data-label-hidden keeps it for screen readers only)\n    data-placeholder  input placeholder\n    data-recent       comma list of values shown as a \"Recent\" group while the query is empty\n    data-noun         word for the footer count (\"zones\" → \"32 zones\", \"3 of 32\")\n    data-multiple     same as <select multiple>: chips, the list stays open while you pick\n                      (live: set it to \"false\" or remove it to go back to one value)\n    data-disabled     disables the whole control\n  Option attributes:\n    data-sub          second line (searched and highlighted)\n    data-keywords     extra hidden search terms (aliases, codes)\n    data-meta         right-hand text (mono)\n    data-code         2–3 letter monogram shown on the left\n    data-tz           an IANA zone: the left glyph becomes a live mini clock and the right\n                      side shows its local time and GMT offset\n    disabled          skipped by the keyboard, not selectable\n  Variables (set on .vs-combobox): --vs-combobox-scale (everything is in em), -width,\n    -font, -mono, -accent, -bg, -field, -pop, -ink, -dim, -line, -radius, -rows (visible\n    rows before scrolling), -stiffness (pill spring).\n  Keys: ↓/↑ open and move, Home/End, PageUp/PageDown, Enter picks, Esc closes (and\n    restores the text), Tab accepts the active row while you are typing, Backspace on an\n    empty input removes the last chip, Alt+↓ opens without moving.\n  Events: \"vs-combobox-change\" on the root with detail { value, values, label }; the\n    <select> also fires \"change\". Methods: el.vsOpen(), el.vsClose(), el.vsSetValue(v|[v]).\n  Reduced motion: every spring snaps; colours still change.\n-->\n<style>\n.vs-combobox {\n  --vs-combobox-scale: 1;\n  --vs-combobox-width: 24em;\n  --vs-combobox-font: inherit;\n  --vs-combobox-mono: ui-monospace, 'SF Mono', Menlo, monospace;\n  --vs-combobox-accent: #7cc4ff;                  /* focus ring, caret, matches, check */\n  --vs-combobox-field: #17191c;                   /* input surface */\n  --vs-combobox-pop: #17191d;                     /* popover surface */\n  --vs-combobox-foot: #121417;                    /* popover footer */\n  --vs-combobox-ink: #eceef1;\n  --vs-combobox-dim: rgba(236,238,241,.5);\n  --vs-combobox-faint: rgba(236,238,241,.3);\n  --vs-combobox-line: rgba(255,255,255,.09);      /* sub-pixel rings */\n  --vs-combobox-hl: rgba(255,255,255,.065);       /* the springing pill */\n  --vs-combobox-radius: .9em;\n  --vs-combobox-rows: 6.5;\n  --vs-combobox-stiffness: 620;\n\n  position: relative; display: inline-grid; gap: .55em; width: min(100%, var(--vs-combobox-width));\n  font-family: var(--vs-combobox-font); font-size: calc(14px * var(--vs-combobox-scale)); line-height: 1.3;\n  color: var(--vs-combobox-ink); -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent;\n  text-align: left;\n}\n.vs-combobox *, .vs-combobox *::before, .vs-combobox *::after { box-sizing: border-box; }\n.vs-combobox > select { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; clip: rect(0 0 0 0); }\n.vs-combobox-label { font-size: .86em; font-weight: 500; color: var(--vs-combobox-dim); padding-left: .15em; letter-spacing: .005em; cursor: default; }\n.vs-combobox-label.is-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }\n\n/* ── field ── */\n.vs-combobox-field {\n  position: relative; display: flex; align-items: center; flex-wrap: wrap; gap: .35em; min-height: 3.15em;\n  padding: .4em .45em .4em 2.75em; border-radius: var(--vs-combobox-radius); cursor: text;\n  background: linear-gradient(180deg, rgba(255,255,255,.028), rgba(255,255,255,0) 60%), var(--vs-combobox-field);\n  box-shadow: 0 0 0 .5px var(--vs-combobox-line), inset 0 .5px 0 rgba(255,255,255,.07), 0 1px 2px rgba(0,0,0,.35), 0 10px 24px -14px rgba(0,0,0,.7);\n  transition: box-shadow .22s cubic-bezier(.2,0,0,1), background-color .22s;\n}\n.vs-combobox-field:hover { box-shadow: 0 0 0 .5px color-mix(in srgb, var(--vs-combobox-line), var(--vs-combobox-ink) 14%), inset 0 .5px 0 rgba(255,255,255,.08), 0 1px 2px rgba(0,0,0,.35), 0 10px 24px -14px rgba(0,0,0,.7); }\n.vs-combobox.is-focus .vs-combobox-field {\n  box-shadow: 0 0 0 1px color-mix(in srgb, var(--vs-combobox-accent) 70%, transparent), 0 0 0 4.5px color-mix(in srgb, var(--vs-combobox-accent) 14%, transparent),\n    inset 0 .5px 0 rgba(255,255,255,.08), 0 10px 30px -12px color-mix(in srgb, var(--vs-combobox-accent) 30%, transparent);\n}\n.vs-combobox-lead { position: absolute; left: 1em; top: 50%; width: 1.15em; height: 1.15em; margin-top: -.575em; color: var(--vs-combobox-dim); transition: color .2s; pointer-events: none; }\n.vs-combobox.is-focus .vs-combobox-lead { color: var(--vs-combobox-accent); }\n.vs-combobox-lead svg { display: block; width: 100%; height: 100%; }\n.vs-combobox-input {\n  flex: 1 1 6em; min-width: 4em; height: 2.3em; border: 0; outline: 0; padding: 0 .15em; margin: 0; background: none;\n  font: inherit; font-size: 1.04em; font-weight: 400; letter-spacing: -.005em; color: inherit; caret-color: var(--vs-combobox-accent);\n  text-overflow: ellipsis;\n}\n.vs-combobox-input::placeholder { color: var(--vs-combobox-faint); }\n.vs-combobox-input::selection { background: color-mix(in srgb, var(--vs-combobox-accent) 32%, transparent); }\n.vs-combobox-btns { display: flex; align-items: center; gap: .1em; margin-left: auto; align-self: center; }\n.vs-combobox-btn {\n  display: grid; place-items: center; width: 2.1em; height: 2.1em; padding: 0; border: 0; border-radius: .6em; background: none;\n  color: var(--vs-combobox-dim); cursor: pointer; transition: background .15s, color .15s, transform .2s cubic-bezier(.34,1.56,.64,1), opacity .2s;\n  -webkit-tap-highlight-color: transparent;\n}\n.vs-combobox-btn svg { width: 1em; height: 1em; }\n.vs-combobox-btn:hover { background: rgba(255,255,255,.06); color: var(--vs-combobox-ink); }\n.vs-combobox-btn:active { transform: scale(.88); }\n.vs-combobox-btn:focus-visible { outline: 1.5px solid var(--vs-combobox-accent); outline-offset: 1px; }\n.vs-combobox-clear { width: 1.7em; height: 1.7em; border-radius: 50%; }\n.vs-combobox-clear svg { width: .8em; height: .8em; }\n.vs-combobox-clear[hidden] { display: none; }\n.vs-combobox-toggle svg { transition: transform .45s cubic-bezier(.34,1.56,.64,1); }\n.vs-combobox.is-open .vs-combobox-toggle svg { transform: rotate(180deg); }\n\n/* ── chips (multiple) ── */\n.vs-combobox-chip {\n  display: inline-flex; align-items: center; gap: .35em; height: 2.05em; padding: 0 .3em 0 .7em; border-radius: .6em; white-space: nowrap;\n  font-size: .9em; font-weight: 500; background: color-mix(in srgb, var(--vs-combobox-accent) 13%, transparent); color: color-mix(in srgb, var(--vs-combobox-accent), #fff 55%);\n  box-shadow: inset 0 0 0 .5px color-mix(in srgb, var(--vs-combobox-accent) 34%, transparent), inset 0 .5px 0 rgba(255,255,255,.08);\n}\n.vs-combobox-chip button { display: grid; place-items: center; width: 1.45em; height: 1.45em; padding: 0; border: 0; border-radius: .4em; background: none; color: inherit; opacity: .6; cursor: pointer; transition: opacity .15s, background .15s; }\n.vs-combobox-chip button:hover { opacity: 1; background: color-mix(in srgb, var(--vs-combobox-accent) 20%, transparent); }\n.vs-combobox-chip svg { width: .7em; height: .7em; }\n\n/* ── popover ── */\n.vs-combobox-pop {\n  position: absolute; left: 0; right: 0; top: calc(100% + .5em); z-index: 60; border-radius: calc(var(--vs-combobox-radius) + .15em);\n  background: var(--vs-combobox-pop); -webkit-backdrop-filter: blur(20px) saturate(1.4); backdrop-filter: blur(20px) saturate(1.4);\n  box-shadow: 0 0 0 .5px rgba(255,255,255,.1), inset 0 .5px 0 rgba(255,255,255,.08), 0 2px 4px rgba(0,0,0,.3), 0 18px 40px -12px rgba(0,0,0,.65), 0 50px 90px -30px rgba(0,0,0,.6);\n  overflow: hidden; transform-origin: 50% 0; visibility: hidden; opacity: 0; will-change: transform, opacity;\n}\n.vs-combobox.is-up .vs-combobox-pop { top: auto; bottom: calc(100% + .5em); transform-origin: 50% 100%; }\n.vs-combobox-scroll {\n  position: relative; max-height: calc(var(--vs-combobox-rows) * 3.2em + 2.4em); overflow-y: auto; overscroll-behavior: contain;\n  padding: .35em .35em 2.2em; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.14) transparent;\n}\n.vs-combobox-list { position: relative; outline: none; }\n.vs-combobox-pill {\n  position: absolute; left: 0; right: 0; top: 0; height: 3em; border-radius: calc(var(--vs-combobox-radius) - .2em); pointer-events: none; opacity: 0;\n  background: var(--vs-combobox-hl); box-shadow: inset 0 0 0 .5px rgba(255,255,255,.06), inset 0 .5px 0 rgba(255,255,255,.05);\n  will-change: transform, opacity;\n}\n.vs-combobox-pill::before { /* the ice rail on the leading edge */\n  content: \"\"; position: absolute; left: 0; top: 28%; bottom: 28%; width: 2px; border-radius: 2px; background: var(--vs-combobox-accent);\n  box-shadow: 0 0 10px var(--vs-combobox-accent);\n}\n.vs-combobox-group + .vs-combobox-group { margin-top: .3em; }\n.vs-combobox-gh {\n  display: flex; justify-content: space-between; padding: .85em .85em .45em; font: 400 .7em/1 var(--vs-combobox-mono);\n  letter-spacing: .14em; text-transform: uppercase; color: var(--vs-combobox-faint); cursor: default; user-select: none;\n}\n.vs-combobox-opt {\n  position: relative; display: grid; grid-template-columns: 1.7em 1fr auto 1.1em; align-items: center; column-gap: .8em; height: 3.2em; padding: 0 .75em 0 .8em;\n  border-radius: calc(var(--vs-combobox-radius) - .2em); cursor: pointer; user-select: none; -webkit-user-select: none;\n}\n.vs-combobox-opt[aria-disabled=\"true\"] { opacity: .35; cursor: not-allowed; }\n.vs-combobox-t { min-width: 0; display: grid; gap: .12em; }\n.vs-combobox-name { font-size: 1em; font-weight: 500; letter-spacing: -.01em; color: color-mix(in srgb, var(--vs-combobox-ink) 88%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color .15s; }\n.vs-combobox-sub { font-size: .8em; color: var(--vs-combobox-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.vs-combobox-opt.is-active .vs-combobox-name { color: var(--vs-combobox-ink); }\n.vs-combobox-hl { background: none; color: var(--vs-combobox-accent); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .22em; text-decoration-color: color-mix(in srgb, var(--vs-combobox-accent) 45%, transparent); }\n.vs-combobox-meta { display: grid; justify-items: end; gap: .2em; font-variant-numeric: tabular-nums; }\n.vs-combobox-meta b { font-weight: 500; font-size: .93em; letter-spacing: -.01em; color: color-mix(in srgb, var(--vs-combobox-ink) 82%, transparent); }\n.vs-combobox-meta i { font: normal 400 .68em/1 var(--vs-combobox-mono); letter-spacing: .04em; color: var(--vs-combobox-faint); }\n.vs-combobox-meta i:only-child { font-size: .74em; }\n.vs-combobox-code {\n  display: grid; place-items: center; width: 1.7em; height: 1.7em; border-radius: .5em; font: 400 .62em/1 var(--vs-combobox-mono); letter-spacing: .02em;\n  background: rgba(255,255,255,.05); box-shadow: inset 0 0 0 .5px rgba(255,255,255,.08); color: var(--vs-combobox-dim);\n}\n.vs-combobox-clock { display: block; width: 1.7em; height: 1.7em; overflow: visible; }\n.vs-combobox-clock circle { fill: rgba(255,255,255,.9); stroke: rgba(255,255,255,.15); stroke-width: .5; }\n.vs-combobox-clock line { stroke: #111317; stroke-width: 1.5; stroke-linecap: round; }\n.vs-combobox-clock line + line { stroke-width: 1.1; }\n.vs-combobox-clock .is-dot { fill: var(--vs-combobox-accent); stroke: none; }\n.vs-combobox-clock.is-night circle { fill: #22262d; stroke: rgba(255,255,255,.14); }\n.vs-combobox-clock.is-night line { stroke: var(--vs-combobox-accent); }\n.vs-combobox-check { width: 1.1em; height: 1.1em; color: var(--vs-combobox-accent); }\n.vs-combobox-check path { stroke-dasharray: 20; stroke-dashoffset: 20; transition: stroke-dashoffset .38s cubic-bezier(.65,0,.35,1); }\n.vs-combobox-opt[aria-selected=\"true\"] .vs-combobox-check path { stroke-dashoffset: 0; }\n.vs-combobox-opt[aria-selected=\"true\"] .vs-combobox-name { color: var(--vs-combobox-ink); }\n.vs-combobox-empty { display: none; padding: 1.9em 1em 1.5em; text-align: center; }\n.vs-combobox.is-empty .vs-combobox-empty { display: grid; justify-items: center; gap: .45em; }\n.vs-combobox-empty svg { width: 1.6em; height: 1.6em; color: var(--vs-combobox-faint); margin-bottom: .3em; }\n.vs-combobox-empty strong { font-weight: 500; font-size: .98em; }\n.vs-combobox-empty span { font-size: .82em; color: var(--vs-combobox-dim); }\n.vs-combobox-foot {\n  position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 1em; height: 2.6em; padding: 0 1em;\n  font: 400 .68em/1 var(--vs-combobox-mono); letter-spacing: .06em; color: var(--vs-combobox-faint);\n  background: var(--vs-combobox-foot); box-shadow: inset 0 .5px 0 rgba(255,255,255,.07);\n}\n.vs-combobox-foot::before { /* rows fade out as they slide under the footer */\n  content: \"\"; position: absolute; left: 0; right: 0; bottom: 100%; height: 2.2em; pointer-events: none;\n  background: linear-gradient(to top, var(--vs-combobox-pop), color-mix(in srgb, var(--vs-combobox-pop) 0%, transparent));\n}\n.vs-combobox-foot span { display: inline-flex; align-items: center; gap: .45em; }\n.vs-combobox-foot kbd {\n  display: inline-grid; place-items: center; min-width: 1.75em; height: 1.75em; padding: 0 .35em; border-radius: .4em; font: inherit; color: var(--vs-combobox-dim);\n  background: rgba(255,255,255,.05); box-shadow: inset 0 0 0 .5px rgba(255,255,255,.09), inset 0 -1px 0 rgba(0,0,0,.35);\n}\n.vs-combobox-count { margin-left: auto; font-variant-numeric: tabular-nums; }\n.vs-combobox-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.vs-combobox[data-disabled] { opacity: .45; pointer-events: none; }\n@media (hover: none) { .vs-combobox-foot span:not(.vs-combobox-count) { display: none; } }\n@media (forced-colors: active) { .vs-combobox-pill { outline: 2px solid Highlight; } .vs-combobox-field, .vs-combobox-pop { border: 1px solid CanvasText; } }\n</style>\n\n<div class=\"vs-combobox\" data-label=\"Home time zone\" data-placeholder=\"City, country or airport\" data-noun=\"zones\" data-recent=\"Europe/Lisbon,Asia/Tokyo,America/New_York\">\n  <select name=\"timezone\">\n    <optgroup label=\"Europe\">\n      <option value=\"Europe/Lisbon\" data-tz=\"Europe/Lisbon\" data-sub=\"Portugal · LIS\" data-keywords=\"lisboa wet\" selected>Lisbon</option>\n      <option value=\"Europe/London\" data-tz=\"Europe/London\" data-sub=\"United Kingdom · LHR\" data-keywords=\"uk england gmt bst\">London</option>\n      <option value=\"Europe/Paris\" data-tz=\"Europe/Paris\" data-sub=\"France · CDG\" data-keywords=\"cet\">Paris</option>\n      <option value=\"Europe/Berlin\" data-tz=\"Europe/Berlin\" data-sub=\"Germany · BER\" data-keywords=\"cet deutschland\">Berlin</option>\n      <option value=\"Europe/Stockholm\" data-tz=\"Europe/Stockholm\" data-sub=\"Sweden · ARN\" data-keywords=\"sverige\">Stockholm</option>\n      <option value=\"Europe/Athens\" data-tz=\"Europe/Athens\" data-sub=\"Greece · ATH\" data-keywords=\"eet\">Athens</option>\n      <option value=\"Atlantic/Reykjavik\" data-tz=\"Atlantic/Reykjavik\" data-sub=\"Iceland · KEF\" data-keywords=\"island\">Reykjavík</option>\n    </optgroup>\n    <optgroup label=\"Asia-Pacific\">\n      <option value=\"Asia/Tokyo\" data-tz=\"Asia/Tokyo\" data-sub=\"Japan · HND\" data-keywords=\"jst nippon\">Tokyo</option>\n      <option value=\"Asia/Seoul\" data-tz=\"Asia/Seoul\" data-sub=\"South Korea · ICN\" data-keywords=\"kst korea\">Seoul</option>\n      <option value=\"Asia/Singapore\" data-tz=\"Asia/Singapore\" data-sub=\"Singapore · SIN\" data-keywords=\"sgt\">Singapore</option>\n      <option value=\"Asia/Hong_Kong\" data-tz=\"Asia/Hong_Kong\" data-sub=\"Hong Kong · HKG\" data-keywords=\"hkt\">Hong Kong</option>\n      <option value=\"Asia/Kolkata\" data-tz=\"Asia/Kolkata\" data-sub=\"India · BOM\" data-keywords=\"ist bombay\">Mumbai</option>\n      <option value=\"Asia/Bangkok\" data-tz=\"Asia/Bangkok\" data-sub=\"Thailand · BKK\" data-keywords=\"ict\">Bangkok</option>\n      <option value=\"Australia/Sydney\" data-tz=\"Australia/Sydney\" data-sub=\"Australia · SYD\" data-keywords=\"aest aedt\">Sydney</option>\n      <option value=\"Pacific/Auckland\" data-tz=\"Pacific/Auckland\" data-sub=\"New Zealand · AKL\" data-keywords=\"nzst aotearoa\">Auckland</option>\n    </optgroup>\n    <optgroup label=\"Americas\">\n      <option value=\"America/New_York\" data-tz=\"America/New_York\" data-sub=\"United States · JFK\" data-keywords=\"nyc est edt eastern\">New York</option>\n      <option value=\"America/Toronto\" data-tz=\"America/Toronto\" data-sub=\"Canada · YYZ\" data-keywords=\"eastern\">Toronto</option>\n      <option value=\"America/Chicago\" data-tz=\"America/Chicago\" data-sub=\"United States · ORD\" data-keywords=\"cst central\">Chicago</option>\n      <option value=\"America/Mexico_City\" data-tz=\"America/Mexico_City\" data-sub=\"Mexico · MEX\" data-keywords=\"cdmx\">Mexico City</option>\n      <option value=\"America/Los_Angeles\" data-tz=\"America/Los_Angeles\" data-sub=\"United States · LAX\" data-keywords=\"pst pdt pacific la\">Los Angeles</option>\n      <option value=\"America/Sao_Paulo\" data-tz=\"America/Sao_Paulo\" data-sub=\"Brazil · GRU\" data-keywords=\"brt\">São Paulo</option>\n      <option value=\"America/Argentina/Buenos_Aires\" data-tz=\"America/Argentina/Buenos_Aires\" data-sub=\"Argentina · EZE\" data-keywords=\"art\">Buenos Aires</option>\n    </optgroup>\n    <optgroup label=\"Africa & Middle East\">\n      <option value=\"Asia/Dubai\" data-tz=\"Asia/Dubai\" data-sub=\"United Arab Emirates · DXB\" data-keywords=\"gst uae\">Dubai</option>\n      <option value=\"Africa/Nairobi\" data-tz=\"Africa/Nairobi\" data-sub=\"Kenya · NBO\" data-keywords=\"eat\">Nairobi</option>\n      <option value=\"Africa/Cairo\" data-tz=\"Africa/Cairo\" data-sub=\"Egypt · CAI\" data-keywords=\"eet\">Cairo</option>\n      <option value=\"Africa/Johannesburg\" data-tz=\"Africa/Johannesburg\" data-sub=\"South Africa · JNB\" data-keywords=\"sast\">Johannesburg</option>\n    </optgroup>\n  </select>\n</div>\n\n<script>\n(() => {\n  if (window.__vsCombobox) return; window.__vsCombobox = true;\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const svg = (d, sw = 1.7) => `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">${d}</svg>`;\n  const I = {\n    lead: svg('<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M3.5 12h17M12 3.5c2.4 2.3 3.6 5.2 3.6 8.5s-1.2 6.2-3.6 8.5c-2.4-2.3-3.6-5.2-3.6-8.5S9.6 5.8 12 3.5z\"/>', 1.5),\n    chev: svg('<path d=\"m7 10 5 5 5-5\"/>', 1.8),\n    x: svg('<path d=\"M6 6l12 12M18 6 6 18\"/>', 2.4),\n    check: '<svg class=\"vs-combobox-check\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m5 12.5 4.5 4.5L19 7.5\"/></svg>',\n    none: svg('<circle cx=\"11\" cy=\"11\" r=\"6.5\"/><path d=\"m16 16 4 4M8.5 11h5\"/>', 1.5)\n  };\n  const esc = (s) => String(s).replace(/[&<>\"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]));\n  // accent-folded lowercase that keeps every index aligned with the original string\n  const fold = (s) => { let o = ''; for (const ch of s) { const f = ch.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase(); o += f.length === ch.length ? f : ch.toLowerCase(); } return o; };\n  // prefix 100 · word start 80 · substring 60 · fuzzy subsequence < 40 (labels only) ; returns [score, ranges]\n  function score(text, q, fuzzy) {\n    if (!q) return [1, []];\n    const t = fold(text), i = t.indexOf(q);\n    if (i === 0) return [100 - t.length * .05, [[0, q.length]]];\n    if (i > 0 && q.length === 1 && !/[\\s\\-·(/]/.test(t[i - 1]) && t.indexOf(' ' + q) < 0) return [0, []]; // one letter: prefix or word start only\n    if (i > 0) { const ws = t.indexOf(' ' + q) >= 0 || /[\\s\\-·(/]/.test(t[i - 1]); const j = ws ? (t.indexOf(' ' + q) + 1 || i) : i; return [(ws ? 80 : 60) - j * .1, [[j, j + q.length]]]; }\n    if (!fuzzy) return [0, []];\n    let s = 0, k = 0, last = -2; const r = [];\n    for (let p = 0; p < t.length && k < q.length; p++) {\n      if (t[p] !== q[k]) continue;\n      s += (p === last + 1 ? 3 : 1) + (p === 0 || t[p - 1] === ' ' ? 2 : 0);\n      if (p === last + 1 && r.length) r[r.length - 1][1] = p + 1; else r.push([p, p + 1]);\n      last = p; k++;\n    }\n    return k === q.length && q.length > 2 ? [Math.min(39, s * 2 - r.length * 2), r] : [0, []];\n  }\n  const mark = (text, ranges) => { if (!ranges || !ranges.length) return esc(text); let o = '', a = 0; for (const [s, e] of ranges) { o += esc(text.slice(a, s)) + '<mark class=\"vs-combobox-hl\">' + esc(text.slice(s, e)) + '</mark>'; a = e; } return o + esc(text.slice(a)); };\n  const tzCache = {};\n  function tzInfo(tz, now) {\n    try {\n      const f = tzCache[tz] || (tzCache[tz] = new Intl.DateTimeFormat('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit', hourCycle: 'h23', timeZoneName: 'shortOffset' }));\n      const parts = Object.fromEntries(f.formatToParts(now).map((p) => [p.type, p.value]));\n      return { h: +parts.hour, m: +parts.minute, time: parts.hour + ':' + parts.minute, off: parts.timeZoneName === 'GMT' ? 'GMT±0' : parts.timeZoneName };\n    } catch (e) { return null; }\n  }\n  const clock = (z) => { const ha = ((z.h % 12) + z.m / 60) * 30, ma = z.m * 6, night = z.h < 6 || z.h >= 19;\n    return `<svg class=\"vs-combobox-clock${night ? ' is-night' : ''}\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10.5\"/><line x1=\"12\" y1=\"12\" x2=\"12\" y2=\"6.6\" transform=\"rotate(${ha.toFixed(1)} 12 12)\"/><line x1=\"12\" y1=\"12\" x2=\"12\" y2=\"4.4\" transform=\"rotate(${ma} 12 12)\"/><circle class=\"is-dot\" cx=\"12\" cy=\"12\" r=\"1.4\"/></svg>`; };\n\n  // one semi-implicit Euler spring; interruptible because x and v carry over when the goal moves\n  const spring = (x, k, c) => ({ x, v: 0, g: x, k, c });\n  const step = (s, dt) => { const n = Math.max(1, Math.round(dt * 240)), h = dt / n; for (let i = 0; i < n; i++) { s.v += (-s.k * (s.x - s.g) - s.c * s.v) * h; s.x += s.v * h; } return Math.abs(s.v) > .01 || Math.abs(s.x - s.g) > .01; };\n  let uid = 0;\n\n  function mount(el) {\n    if (el.__vsCombo) return; el.__vsCombo = true;\n    const sel = el.querySelector('select'); if (!sel) return;\n    const id = 'vs-cbx-' + (++uid); let multi = sel.multiple || (el.hasAttribute('data-multiple') && el.dataset.multiple !== 'false');\n    if (multi) sel.multiple = true;\n    const noun = el.dataset.noun || 'results';\n    // read the <select> into data\n    const groups = [], all = [];\n    const read = (o, g) => ({ value: o.value, label: o.textContent.trim(), sub: o.dataset.sub || '', kw: o.dataset.keywords || '', meta: o.dataset.meta || '', code: o.dataset.code || '', tz: o.dataset.tz || '', disabled: o.disabled, group: g, el: o });\n    let loose = null;\n    for (const n of sel.children) {\n      if (n.tagName === 'OPTGROUP') { const g = { label: n.label, items: [] }; for (const o of n.children) { const d = read(o, g); g.items.push(d); all.push(d); } groups.push(g); }\n      else if (n.tagName === 'OPTION' && n.value !== '') { if (!loose) { loose = { label: '', items: [] }; groups.unshift(loose); } const d = read(n, loose); loose.items.push(d); all.push(d); }\n    }\n    const byVal = new Map(all.map((d) => [d.value, d]));\n    const recent = (el.dataset.recent || '').split(',').map((v) => byVal.get(v.trim())).filter(Boolean);\n    let values = all.filter((d) => d.el.selected && (multi || d.el.hasAttribute('selected'))).map((d) => d.value);\n    if (!multi) values = values.slice(0, 1);\n\n    // build\n    const lab = document.createElement('label'); lab.className = 'vs-combobox-label'; lab.htmlFor = id + '-in'; lab.id = id + '-lab';\n    lab.textContent = el.dataset.label || sel.getAttribute('aria-label') || 'Choose';\n    if (el.hasAttribute('data-label-hidden')) lab.classList.add('is-hidden');\n    const field = document.createElement('div'); field.className = 'vs-combobox-field';\n    field.innerHTML = `<span class=\"vs-combobox-lead\">${I.lead}</span><input class=\"vs-combobox-input\" id=\"${id}-in\" type=\"text\" role=\"combobox\" aria-autocomplete=\"list\" aria-expanded=\"false\" aria-controls=\"${id}-lb\" aria-labelledby=\"${id}-lab\" autocomplete=\"off\" autocapitalize=\"off\" spellcheck=\"false\" enterkeyhint=\"done\"><span class=\"vs-combobox-btns\"><button type=\"button\" class=\"vs-combobox-btn vs-combobox-clear\" aria-label=\"Clear\" hidden>${I.x}</button><button type=\"button\" class=\"vs-combobox-btn vs-combobox-toggle\" tabindex=\"-1\" aria-label=\"Show options\" aria-controls=\"${id}-lb\" aria-expanded=\"false\">${I.chev}</button></span>`;\n    const pop = document.createElement('div'); pop.className = 'vs-combobox-pop';\n    pop.innerHTML = `<div class=\"vs-combobox-scroll\"><div class=\"vs-combobox-list\" role=\"listbox\" id=\"${id}-lb\" aria-labelledby=\"${id}-lab\"${multi ? ' aria-multiselectable=\"true\"' : ''}></div><div class=\"vs-combobox-empty\" role=\"presentation\">${I.none}<strong></strong><span>Try a city, a country or a three-letter airport code</span></div></div><div class=\"vs-combobox-foot\" aria-hidden=\"true\"><span><kbd>↑</kbd><kbd>↓</kbd> move</span><span><kbd>↵</kbd> ${multi ? 'toggle' : 'pick'}</span><span><kbd>esc</kbd> close</span><span class=\"vs-combobox-count\"></span></div>`;\n    const live = document.createElement('span'); live.className = 'vs-combobox-sr'; live.setAttribute('aria-live', 'polite');\n    el.prepend(lab); sel.before(field, pop, live);\n    sel.tabIndex = -1; sel.setAttribute('aria-hidden', 'true');\n    if (el.hasAttribute('data-disabled')) field.querySelector('input').disabled = true;\n    const input = field.querySelector('input'), clearB = field.querySelector('.vs-combobox-clear'), tog = field.querySelector('.vs-combobox-toggle');\n    const scroll = pop.querySelector('.vs-combobox-scroll'), list = pop.querySelector('.vs-combobox-list'), emptyT = pop.querySelector('.vs-combobox-empty strong'), count = pop.querySelector('.vs-combobox-count');\n    input.placeholder = el.dataset.placeholder || 'Search';\n    const pill = document.createElement('span'); pill.className = 'vs-combobox-pill'; pill.setAttribute('aria-hidden', 'true');\n\n    let open = false, query = '', rows = [], active = -1, typed = false, liveT = 0;\n    const S = { p: spring(0, 520, 30), y: spring(0, 620, 42), h: spring(0, 600, 48), po: spring(0, 400, 36) };\n    let raf = 0, last = 0;\n    const kStiff = () => { const v = parseFloat(getComputedStyle(el).getPropertyValue('--vs-combobox-stiffness')); return v > 0 ? v : 620; };\n\n    function frame(now) {\n      const dt = Math.min(.05, (now - last) / 1000 || .016); last = now; let busy = false;\n      for (const k in S) { const s = S[k]; if (reduce.matches) { s.x = s.g; s.v = 0; continue; } if (step(s, dt)) busy = true; else { s.x = s.g; s.v = 0; } }\n      const p = S.p.x, up = el.classList.contains('is-up');\n      // open: grows from the field (scale + y), overshoots a hair; close: tucks back, no bounce\n      pop.style.opacity = Math.max(0, Math.min(1, p * 1.6)).toFixed(3);\n      pop.style.transform = `translate3d(0,${((1 - p) * (up ? 10 : -10)).toFixed(2)}px,0) scale(${(.94 + .06 * p).toFixed(4)}, ${(.9 + .1 * p).toFixed(4)})`;\n      if (S.h.x > 0) pop.style.height = S.h.x.toFixed(2) + 'px';\n      // the pill stretches along its travel and settles back\n      const st = Math.min(.18, Math.abs(S.y.v) / 9000);\n      pill.style.transform = `translate3d(0,${S.y.x.toFixed(2)}px,0) scale(${(1 - st * .25).toFixed(4)},${(1 + st).toFixed(4)})`;\n      pill.style.opacity = Math.max(0, Math.min(1, S.po.x)).toFixed(3);\n      if (!busy && S.p.g === 0) { pop.style.visibility = 'hidden'; }\n      raf = busy ? requestAnimationFrame(frame) : 0;\n    }\n    const wake = () => { if (!raf) { last = performance.now(); raf = requestAnimationFrame(frame); } };\n\n    const labelOf = (v) => (byVal.get(v) || {}).label || '';\n    function syncSelect(fire) {\n      for (const d of all) d.el.selected = values.includes(d.value);\n      if (!multi && !values.length) sel.selectedIndex = -1;\n      clearB.hidden = !(values.length || input.value);\n      if (fire) {\n        sel.dispatchEvent(new Event('change', { bubbles: true }));\n        el.dispatchEvent(new CustomEvent('vs-combobox-change', { bubbles: true, detail: { value: values[0] ?? null, values: values.slice(), label: labelOf(values[0]) } }));\n      }\n    }\n    function renderChips() {\n      if (!multi) return;\n      field.querySelectorAll('.vs-combobox-chip').forEach((c) => { if (!values.includes(c.dataset.v)) c.remove(); });\n      for (const v of values) {\n        if (field.querySelector(`.vs-combobox-chip[data-v=\"${CSS.escape(v)}\"]`)) continue;\n        const c = document.createElement('span'); c.className = 'vs-combobox-chip'; c.dataset.v = v;\n        c.innerHTML = `${esc(labelOf(v))}<button type=\"button\" tabindex=\"-1\" aria-label=\"Remove ${esc(labelOf(v))}\">${I.x}</button>`;\n        c.querySelector('button').addEventListener('click', (e) => { e.stopPropagation(); toggle(v); input.focus(); });\n        input.before(c);\n        if (!reduce.matches) c.animate([{ transform: 'scale(.6)', opacity: 0 }, { transform: 'scale(1.04)', opacity: 1, offset: .6 }, { transform: 'none', opacity: 1 }], { duration: 380, easing: 'cubic-bezier(.34,1.4,.64,1)' });\n      }\n      input.placeholder = values.length ? '' : (el.dataset.placeholder || 'Search');\n    }\n    function showValue() { if (!multi) input.value = labelOf(values[0]); clearB.hidden = !(values.length || input.value); }\n\n    // ── build the visible rows for the current query\n    function build(keepActiveValue) {\n      const q = fold(query.trim()), now = new Date();\n      const prevVal = keepActiveValue && rows[active] ? rows[active].d.value : null;\n      let sections = [];\n      if (!q) {\n        if (recent.length) sections.push({ label: 'Recent', items: recent.map((d) => ({ d })) });\n        for (const g of groups) sections.push({ label: g.label, items: g.items.map((d) => ({ d })) });\n      } else {\n        for (const g of groups) {\n          const items = [];\n          for (const d of g.items) {\n            const a = score(d.label, q, true), b = score(d.sub, q), c = score(d.kw, q);\n            const s = Math.max(a[0] * 1.15, b[0] * .9, c[0] * .75);\n            if (s > 0) items.push({ d, s, ra: a[0] ? a[1] : null, rb: b[0] && !a[0] ? b[1] : null });\n          }\n          if (items.length) { items.sort((x, y) => y.s - x.s); sections.push({ label: g.label, items, best: items[0].s }); }\n        }\n        sections.sort((a, b) => b.best - a.best);\n      }\n      let html = '', n = 0;\n      rows = [];\n      sections.forEach((sec, gi) => {\n        const gid = `${id}-g${gi}`;\n        html += `<div class=\"vs-combobox-group\" role=\"group\"${sec.label ? ` aria-labelledby=\"${gid}\"` : ''}>` + (sec.label ? `<div class=\"vs-combobox-gh\" id=\"${gid}\" role=\"presentation\"><span>${esc(sec.label)}</span>${q ? '' : `<span>${sec.items.length}</span>`}</div>` : '');\n        for (const it of sec.items) {\n          const d = it.d, oid = `${id}-o${n}`, z = d.tz ? tzInfo(d.tz, now) : null;\n          const lead = z ? clock(z) : `<span class=\"vs-combobox-code\">${esc(d.code || d.label.slice(0, 2).toUpperCase())}</span>`;\n          const meta = z ? `<b>${z.time}</b><i>${esc(z.off)}</i>` : d.meta ? `<i>${esc(d.meta)}</i>` : '';\n          html += `<div class=\"vs-combobox-opt\" role=\"option\" id=\"${oid}\" data-i=\"${n}\" aria-selected=\"${values.includes(d.value)}\"${d.disabled ? ' aria-disabled=\"true\"' : ''}>${lead}<span class=\"vs-combobox-t\"><span class=\"vs-combobox-name\">${mark(d.label, it.ra)}</span>${d.sub ? `<span class=\"vs-combobox-sub\">${mark(d.sub, it.rb)}</span>` : ''}</span><span class=\"vs-combobox-meta\">${meta}</span>${I.check}</div>`;\n          rows.push({ d, id: oid }); n++;\n        }\n        html += '</div>';\n      });\n      list.innerHTML = html; list.prepend(pill);\n      rows.forEach((r, i) => { r.el = document.getElementById(r.id); });\n      el.classList.toggle('is-empty', !rows.length);\n      emptyT.textContent = q ? `Nothing matches “${query.trim()}”` : 'No options';\n      const total = all.length;\n      count.textContent = q ? `${rows.length} of ${total}` : `${total} ${noun}`;\n      // choose the active row: keep it, else the selected value (no query), else the best match\n      let ai = -1;\n      if (prevVal != null) ai = rows.findIndex((r) => r.d.value === prevVal);\n      if (ai < 0 && !q && values.length) ai = rows.findIndex((r) => r.d.value === values[values.length - 1]);\n      if (ai < 0) ai = rows.findIndex((r) => !r.d.disabled);\n      setActive(ai, { snap: true, scroll: true });\n      measure(false);\n      clearTimeout(liveT);\n      if (q) liveT = setTimeout(() => { live.textContent = rows.length ? `${rows.length} ${rows.length === 1 ? 'result' : 'results'} available` : 'No results'; }, 450);\n    }\n    function measure(snap) {\n      const inner = scroll.scrollHeight, max = parseFloat(getComputedStyle(scroll).maxHeight) || inner;\n      S.h.g = Math.min(inner, max);\n      if (snap || !open) { S.h.x = S.h.g; S.h.v = 0; }\n      wake();\n    }\n    function setActive(i, o = {}) {\n      rows.forEach((r, k) => r.el.classList.toggle('is-active', k === i));\n      active = i;\n      if (i < 0 || !rows[i]) { input.removeAttribute('aria-activedescendant'); S.po.g = 0; wake(); return; }\n      const r = rows[i].el;\n      if (open) input.setAttribute('aria-activedescendant', r.id);\n      pill.style.height = r.offsetHeight + 'px';\n      S.y.k = kStiff(); S.y.c = 2 * Math.sqrt(S.y.k) * .86;\n      S.y.g = r.offsetTop;\n      if (o.snap || S.po.x < .05) { S.y.x = S.y.g; S.y.v = 0; }\n      S.po.g = 1; if (o.snap && !open) S.po.x = 1;\n      if (o.scroll !== false) {\n        const top = r.offsetTop, bot = top + r.offsetHeight, pad = 10, foot = pop.querySelector('.vs-combobox-foot').offsetHeight;\n        const header = r.parentNode.firstElementChild.classList.contains('vs-combobox-gh') && r.previousElementSibling === r.parentNode.firstElementChild ? r.previousElementSibling.offsetHeight : 0;\n        if (top - header < scroll.scrollTop + pad) scroll.scrollTop = Math.max(0, top - header - pad);\n        else if (bot > scroll.scrollTop + scroll.clientHeight - foot - pad) scroll.scrollTop = bot - scroll.clientHeight + foot + pad + 4;\n      }\n      wake();\n    }\n    function move(delta, abs) {\n      if (!rows.length) return;\n      let i = abs != null ? abs : active + delta;\n      i = Math.max(0, Math.min(rows.length - 1, i));\n      const dir = delta || (abs === 0 ? 1 : -1);\n      while (rows[i] && rows[i].d.disabled) i += dir;\n      if (!rows[i]) return;\n      setActive(i);\n    }\n    // flip above the field when there is no room below, and never run off-screen\n    function place() {\n      const r = field.getBoundingClientRect(), below = innerHeight - r.bottom, above = r.top;\n      scroll.style.maxHeight = '';\n      const def = parseFloat(getComputedStyle(scroll).maxHeight) || 300, need = Math.min(scroll.scrollHeight, def) + 16;\n      const up = below < need && above > below;\n      el.classList.toggle('is-up', up);\n      pop.style.bottom = up ? (el.offsetHeight - field.offsetTop + field.offsetHeight * .16) + 'px' : '';\n      const room = (up ? above : below) - 24;\n      if (room < def) scroll.style.maxHeight = Math.max(140, room) + 'px';\n    }\n    function setOpen(v) {\n      if (v === open) return; open = v;\n      el.classList.toggle('is-open', v);\n      input.setAttribute('aria-expanded', v); tog.setAttribute('aria-expanded', v); tog.setAttribute('aria-label', v ? 'Hide options' : 'Show options');\n      if (v) {\n        pop.style.visibility = 'visible';\n        place(); build(false); place(); measure(true);\n        S.p.k = 520; S.p.c = 30; S.p.g = 1;\n        // rows fade up in a quick stagger (only on a fresh open)\n        if (!reduce.matches && S.p.x < .3) rows.slice(0, 9).forEach((r, i) => r.el.animate([{ opacity: 0, transform: 'translateY(6px)' }, { opacity: 1, transform: 'none' }], { duration: 300, delay: 30 + i * 18, easing: 'cubic-bezier(.2,.8,.2,1)', fill: 'backwards' }));\n      } else {\n        S.p.k = 700; S.p.c = 56; S.p.g = 0;\n        input.removeAttribute('aria-activedescendant');\n      }\n      wake();\n    }\n    function toggle(v) {\n      const d = byVal.get(v); if (!d || d.disabled) return;\n      if (multi) { values = values.includes(v) ? values.filter((x) => x !== v) : values.concat(v); renderChips(); }\n      else values = [v];\n      rows.forEach((r) => r.el.setAttribute('aria-selected', values.includes(r.d.value)));\n      syncSelect(true);\n      live.textContent = multi ? `${d.label} ${values.includes(v) ? 'added' : 'removed'}, ${values.length} selected` : `${d.label} selected`;\n    }\n    function commit(i) {\n      const r = rows[i]; if (!r || r.d.disabled) return;\n      toggle(r.d.value);\n      if (multi) { if (query) { query = ''; input.value = ''; build(true); } }\n      else { query = ''; typed = false; showValue(); setOpen(false); input.select(); }\n    }\n    function restore() { query = ''; typed = false; showValue(); if (multi) input.value = ''; clearB.hidden = !(values.length || input.value); }\n\n    input.addEventListener('input', () => {\n      query = input.value; typed = true; clearB.hidden = !(values.length || input.value);\n      if (!open) setOpen(true); else build(false);\n      measure(false); place();\n    });\n    input.addEventListener('keydown', (e) => {\n      if (e.isComposing) return;\n      const k = e.key;\n      if (k === 'ArrowDown' || k === 'ArrowUp') {\n        e.preventDefault();\n        if (!open) { setOpen(true); if (!e.altKey && k === 'ArrowUp') move(0, rows.length - 1); return; }\n        if (e.altKey && k === 'ArrowUp') { if (active >= 0) commit(active); else setOpen(false); return; }\n        move(k === 'ArrowDown' ? 1 : -1);\n      } else if ((k === 'Home' || k === 'End') && open && !e.shiftKey) { e.preventDefault(); move(0, k === 'Home' ? 0 : rows.length - 1); }\n      else if ((k === 'PageDown' || k === 'PageUp') && open) { e.preventDefault(); move(k === 'PageDown' ? 5 : -5); }\n      else if (k === 'Enter') { if (open && active >= 0) { e.preventDefault(); commit(active); } else if (!open) { e.preventDefault(); setOpen(true); } }\n      else if (k === 'Escape') {\n        if (open) { e.preventDefault(); setOpen(false); restore(); input.select(); }\n        else if (!multi && input.value && !values.length) { input.value = ''; clearB.hidden = true; }\n      } else if (k === 'Tab') { if (open) { if (typed && query && active >= 0) commit(active); setOpen(false); restore(); } }\n      else if (k === 'Backspace' && multi && !input.value && values.length) { toggle(values[values.length - 1]); if (open) build(true); }\n    });\n    // focusing a single-value field selects its text, so typing replaces the choice\n    let selUp = false;\n    input.addEventListener('focus', () => { el.classList.add('is-focus'); if (!multi && values.length && input.value === labelOf(values[0])) { input.select(); selUp = true; } });\n    input.addEventListener('mouseup', (e) => { if (selUp) { e.preventDefault(); input.select(); } selUp = false; });\n    input.addEventListener('blur', () => { el.classList.remove('is-focus'); });\n    el.addEventListener('focusout', (e) => { if (!el.contains(e.relatedTarget)) { if (open) setOpen(false); restore(); } });\n    field.addEventListener('mousedown', (e) => { if (e.target !== input && !e.target.closest('button')) { e.preventDefault(); input.focus(); } });\n    field.addEventListener('click', (e) => { if (e.target.closest('.vs-combobox-btns, .vs-combobox-chip')) return; if (!open) { setOpen(true); if (!multi) input.select(); } });\n    tog.addEventListener('mousedown', (e) => e.preventDefault());\n    tog.addEventListener('click', () => { input.focus(); setOpen(!open); if (!open) restore(); });\n    clearB.addEventListener('mousedown', (e) => e.preventDefault());\n    clearB.addEventListener('click', () => {\n      values = []; query = ''; input.value = ''; renderChips(); syncSelect(true); input.focus();\n      if (open) build(false); live.textContent = 'Selection cleared';\n      if (!reduce.matches) input.animate([{ opacity: .2, transform: 'translateX(-4px)' }, { opacity: 1, transform: 'none' }], { duration: 260, easing: 'cubic-bezier(.2,.8,.2,1)' });\n    });\n    // pointer: rows never take focus; hover moves the pill (mouse only, and only when the pointer really moved)\n    pop.addEventListener('mousedown', (e) => e.preventDefault());\n    let lx = -1, ly = -1;\n    list.addEventListener('pointermove', (e) => {\n      if (e.pointerType !== 'mouse' || (e.clientX === lx && e.clientY === ly)) return; lx = e.clientX; ly = e.clientY;\n      const o = e.target.closest('.vs-combobox-opt'); if (!o) return; const i = +o.dataset.i; if (i !== active && !rows[i].d.disabled) setActive(i, { scroll: false });\n    });\n    list.addEventListener('click', (e) => { const o = e.target.closest('.vs-combobox-opt'); if (!o) return; const i = +o.dataset.i; setActive(i, { scroll: false }); commit(i); if (multi) input.focus(); });\n    // keep live clocks honest while open\n    setInterval(() => { if (open && !typed) { const a = rows[active] && rows[active].d.value; build(true); } }, 30000);\n    addEventListener('resize', () => { if (open) { place(); measure(true); if (rows[active]) setActive(active, { snap: true, scroll: false }); } });\n\n    el.vsOpen = () => { input.focus(); setOpen(true); };\n    el.vsClose = () => { setOpen(false); restore(); };\n    el.vsSetValue = (v) => { values = (Array.isArray(v) ? v : v == null ? [] : [v]).filter((x) => byVal.has(x)); if (!multi) values = values.slice(0, 1); renderChips(); syncSelect(true); showValue(); if (open) build(true); };\n    el.vsSetMultiple = (m) => { m = !!m; if (m === multi) return; multi = m; sel.multiple = m; if (!m) values = values.slice(-1); field.querySelectorAll('.vs-combobox-chip').forEach((c) => c.remove()); if (m) list.setAttribute('aria-multiselectable', 'true'); else list.removeAttribute('aria-multiselectable'); pop.querySelector('.vs-combobox-foot span:nth-child(2)').lastChild.textContent = m ? ' toggle' : ' pick'; input.value = ''; renderChips(); showValue(); input.placeholder = m && values.length ? '' : (el.dataset.placeholder || 'Search'); syncSelect(false); if (open) build(true); };\n    Object.defineProperty(el, 'value', { get: () => (multi ? values.slice() : values[0] ?? null), configurable: true });\n\n    // data-multiple can be flipped live\n    new MutationObserver(() => el.vsSetMultiple(el.hasAttribute('data-multiple') && el.dataset.multiple !== 'false')).observe(el, { attributes: true, attributeFilter: ['data-multiple'] });\n    renderChips(); showValue(); syncSelect(false); build(false);\n    pop.style.visibility = 'hidden';\n  }\n  const init = () => document.querySelectorAll('.vs-combobox').forEach(mount);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n  window.vsCombobox = { mount };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Forms",
    "tags": [
      "combobox",
      "select",
      "autocomplete",
      "dropdown",
      "listbox",
      "multi-select",
      "time zone",
      "keyboard",
      "form"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [
      "https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500&family=DM+Mono:wght@400&display=swap"
    ],
    "source": null,
    "free": true,
    "accent": "#7cc4ff",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/combobox/index.html",
    "thumbnail": "https://vividsites.app/api/component/combobox/thumb.jpg",
    "video": "https://vividsites.app/api/component/combobox/thumb.mp4",
    "page": "https://vividsites.app/c/combobox"
  }
}