{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-palette",
  "type": "registry:block",
  "title": "Command Palette",
  "description": "A ⌘K palette that springs open, ranks results with fzf-style fuzzy scoring and highlights the matched letters. A sprung highlight follows the arrow keys, sub-pages push a breadcrumb, and light, dark and density are live CSS variables.",
  "docs": "Command Palette was added to public/vividsites/command-palette.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/command-palette",
  "categories": [
    "ui",
    "buttons-inputs"
  ],
  "files": [
    {
      "path": "components/vividsites/command-palette.html",
      "type": "registry:file",
      "target": "public/vividsites/command-palette.html",
      "content": "<!--\nvs-command-palette: a ⌘K / Ctrl+K palette. It opens on a spring, searches with\n  fzf-style fuzzy scoring (word starts and runs of letters rank first, matches are\n  highlighted), keeps results grouped, and moves a sprung highlight as you arrow through.\n  Items that open a sub-page push a breadcrumb; Backspace on an empty field goes back.\n\n  You write the commands as plain HTML inside .vs-command-palette (it stays hidden):\n    <div data-group=\"Projects\">                         a group, in order\n      <a href=\"/p/harbour\" data-hint=\"Feature\" data-kbd=\"⌘ 1\" data-keywords=\"film\">\n        <svg>…</svg> Harbour Lights                      optional icon, then the title\n      </a>\n      <button data-page=\"theme\">Change theme…</button>  opens the group with data-page-id=\"theme\"\n    </div>\n    <div data-group=\"Theme\" data-page-id=\"theme\"> … </div>   a sub-page (not shown at the root)\n  Choosing an item closes the palette and clicks the original element, so links and your\n  own click handlers just work. The root also fires \"vs-command\" (detail: the element).\n\n  Themes: dark by default; data-theme=\"light\" on .vs-command-palette for the light one.\n  Every --vs-command-palette-* variable set on .vs-command-palette is copied live to the\n  open palette (change a class / style / data-theme and it follows). --…-row sets density.\n  Open with ⌘K / Ctrl+K, any .vs-command-palette-trigger, or el.vsOpen(). Esc closes.\n  data-hotkey on .vs-command-palette changes the letter (default \"k\").\n  Reduced motion: no spring, the palette simply appears.\n-->\n<button class=\"vs-command-palette-trigger\" type=\"button\">Search <kbd>⌘K</kbd></button>\n\n<div class=\"vs-command-palette\" data-placeholder=\"Type a command or search…\">\n  <div data-group=\"Suggestions\">\n    <a href=\"#\" data-hint=\"Project · edited 12 min ago\">Open Harbour Lights</a>\n    <button type=\"button\" data-kbd=\"⌘ ⇧ R\">New review link</button>\n  </div>\n  <div data-group=\"Actions\">\n    <button type=\"button\" data-kbd=\"⌘ E\">Export current reel…</button>\n    <button type=\"button\" data-page=\"theme\">Change theme…</button>\n  </div>\n  <div data-group=\"Theme\" data-page-id=\"theme\">\n    <button type=\"button\">Night</button>\n    <button type=\"button\">Paper</button>\n  </div>\n</div>\n\n<style>\n.vs-command-palette { display: none; }\n/* light theme: put data-theme=\"light\" on .vs-command-palette (the variables are copied live) */\n.vs-command-palette[data-theme=\"light\"] {\n  --vs-command-palette-bg: rgba(252,252,251,.9); --vs-command-palette-ink: #17181b; --vs-command-palette-dim: rgba(23,24,27,.48);\n  --vs-command-palette-line: rgba(23,24,27,.08); --vs-command-palette-accent: #2f6bff; --vs-command-palette-hl: rgba(23,24,27,.055);\n  --vs-command-palette-scrim: rgba(40,38,34,.18); --vs-command-palette-shadow: 0 0 0 1px rgba(0,0,0,.06), 0 1px 0 rgba(255,255,255,.9) inset, 0 40px 80px -30px rgba(30,28,24,.35), 0 12px 28px -12px rgba(30,28,24,.18);\n}\n.vs-command-palette-root {\n  --vs-command-palette-bg: rgba(20,23,27,.86);      /* panel */\n  --vs-command-palette-ink: #eef1f3;                /* text */\n  --vs-command-palette-dim: rgba(238,241,243,.5);   /* hints, group labels */\n  --vs-command-palette-line: rgba(238,241,243,.08); /* hairlines */\n  --vs-command-palette-accent: #6ee7d2;             /* highlight bar, matched letters */\n  --vs-command-palette-hl: rgba(238,241,243,.07);   /* active row */\n  --vs-command-palette-scrim: rgba(6,8,10,.55);\n  --vs-command-palette-radius: 16px;\n  --vs-command-palette-width: 40rem;\n  --vs-command-palette-height: 23rem;               /* the list area; the panel never resizes */\n  --vs-command-palette-font: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, 'Segoe UI', sans-serif;\n  --vs-command-palette-mono: ui-monospace, 'SF Mono', Menlo, monospace;\n  --vs-command-palette-row: 2.85rem;                 /* row height (density) */\n  --vs-command-palette-shadow: 0 0 0 1px rgba(255,255,255,.08), inset 0 1px 0 rgba(255,255,255,.07), 0 40px 90px -30px rgba(0,0,0,.75), 0 12px 30px -12px rgba(0,0,0,.5);\n  --k: color-mix(in srgb, var(--vs-command-palette-ink) 7%, transparent);   /* chips, icons, kbd */\n  --kl: color-mix(in srgb, var(--vs-command-palette-ink) 9%, transparent);\n\n  position: fixed; inset: 0; z-index: 2147483000; display: flex; justify-content: center; align-items: flex-start;\n  padding: min(16vh, 9rem) 16px 16px; font-family: var(--vs-command-palette-font); color: var(--vs-command-palette-ink);\n}\n.vs-command-palette-root[hidden] { display: none; }\n.vs-command-palette-scrim { position: absolute; inset: 0; background: var(--vs-command-palette-scrim); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); opacity: 0; will-change: opacity; }\n.vs-command-palette-panel {\n  position: relative; width: 100%; max-width: var(--vs-command-palette-width); border-radius: var(--vs-command-palette-radius);\n  background: var(--vs-command-palette-bg); -webkit-backdrop-filter: blur(18px) saturate(1.3); backdrop-filter: blur(18px) saturate(1.3);\n  box-shadow: var(--vs-command-palette-shadow);\n  overflow: hidden; opacity: 0; will-change: transform, opacity; transform-origin: 50% 0;\n}\n.vs-command-palette-head { display: flex; align-items: center; gap: .7rem; padding: 0 1.1rem; height: 3.6rem; border-bottom: 1px solid var(--vs-command-palette-line); }\n.vs-command-palette-head > svg { width: 18px; height: 18px; flex: none; opacity: .55; }\n.vs-command-palette-crumb { flex: none; font-size: .78rem; padding: .3rem .55rem; border-radius: 7px; background: var(--kl); color: var(--vs-command-palette-ink); white-space: nowrap; }\n.vs-command-palette-crumb[hidden] { display: none; }\n.vs-command-palette-input {\n  flex: 1; min-width: 0; height: 100%; border: 0; outline: none; background: none; color: inherit;\n  font: inherit; font-size: 1.02rem; letter-spacing: -.005em; caret-color: var(--vs-command-palette-accent);\n}\n.vs-command-palette-input::placeholder { color: var(--vs-command-palette-dim); }\n.vs-command-palette-root kbd, .vs-command-palette-trigger kbd {\n  font: 500 .7rem/1 var(--vs-command-palette-mono); letter-spacing: .02em; color: var(--vs-command-palette-dim, rgba(238,241,243,.5));\n  padding: .28rem .4rem; border-radius: 5px; background: var(--k, rgba(255,255,255,.06)); box-shadow: inset 0 -1px 0 var(--k, rgba(255,255,255,.06)), 0 0 0 1px var(--k, rgba(255,255,255,.06));\n}\n.vs-command-palette-list { position: relative; height: var(--vs-command-palette-height); max-height: 58vh; overflow-y: auto; overscroll-behavior: contain; padding: .45rem .5rem .6rem; scrollbar-width: thin; scrollbar-color: var(--kl) transparent; }\n.vs-command-palette-hl {\n  position: absolute; left: .5rem; right: .5rem; top: 0; height: var(--vs-command-palette-row); border-radius: 10px; background: var(--vs-command-palette-hl);\n  pointer-events: none; will-change: transform; opacity: 0;\n}\n.vs-command-palette-hl::before { content: \"\"; position: absolute; left: 0; top: .8rem; bottom: .8rem; width: 2px; border-radius: 2px; background: var(--vs-command-palette-accent); box-shadow: 0 0 10px var(--vs-command-palette-accent); }\n.vs-command-palette-group { padding: .85rem .8rem .4rem; font-size: .7rem; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--vs-command-palette-dim); }\n.vs-command-palette-group:first-of-type { padding-top: .45rem; }\n.vs-command-palette-item {\n  position: relative; display: flex; align-items: center; gap: .8rem; height: var(--vs-command-palette-row); padding: 0 .8rem; border-radius: 10px;\n  cursor: pointer; user-select: none; color: color-mix(in srgb, var(--vs-command-palette-ink) 84%, transparent);\n}\n.vs-command-palette-item[aria-selected=\"true\"] { color: var(--vs-command-palette-ink); }\n.vs-command-palette-icon { width: 1.75rem; height: 1.75rem; flex: none; display: grid; place-items: center; border-radius: 7px; background: var(--k); box-shadow: inset 0 0 0 1px var(--k); color: var(--vs-command-palette-ink); }\n.vs-command-palette-icon svg { width: 15px; height: 15px; opacity: .85; }\n.vs-command-palette-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: .93rem; }\n.vs-command-palette-title mark { background: none; color: var(--vs-command-palette-accent); font-weight: 600; text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 3px; text-decoration-color: color-mix(in srgb, var(--vs-command-palette-accent) 40%, transparent); }\n.vs-command-palette-hint { margin-left: auto; padding-left: 1rem; white-space: nowrap; font-size: .8rem; color: var(--vs-command-palette-dim); overflow: hidden; text-overflow: ellipsis; }\n.vs-command-palette-keys { display: flex; gap: .25rem; flex: none; margin-left: .4rem; }\n.vs-command-palette-hint + .vs-command-palette-keys { margin-left: .6rem; }\n.vs-command-palette-keys:first-child, .vs-command-palette-title + .vs-command-palette-keys { margin-left: auto; }\n.vs-command-palette-empty { padding: 3.2rem 1rem; text-align: center; color: var(--vs-command-palette-dim); font-size: .92rem; line-height: 1.5; }\n.vs-command-palette-empty b { color: var(--vs-command-palette-ink); font-weight: 500; }\n.vs-command-palette-foot { display: flex; align-items: center; gap: 1rem; height: 2.7rem; padding: 0 .9rem 0 1.1rem; border-top: 1px solid var(--vs-command-palette-line); font-size: .76rem; color: var(--vs-command-palette-dim); background: color-mix(in srgb, var(--vs-command-palette-ink) 3%, transparent); }\n.vs-command-palette-foot span { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; }\n.vs-command-palette-foot .vs-command-palette-act { margin-right: auto; color: var(--vs-command-palette-ink); }\n.vs-command-palette-root, .vs-command-palette-root *, .vs-command-palette-trigger { box-sizing: border-box; }\n.vs-command-palette-trigger {\n  --vs-command-palette-trigger-bg: #15181c;       /* the opener button */\n  --vs-command-palette-trigger-ink: #eef1f3;\n  display: inline-flex; align-items: center; gap: .8rem; cursor: pointer; font: inherit; color: var(--vs-command-palette-trigger-ink);\n  padding: .55rem .6rem .55rem .9rem; border-radius: 10px; border: 0; background: var(--vs-command-palette-trigger-bg);\n  --k: color-mix(in srgb, var(--vs-command-palette-trigger-ink) 8%, transparent);\n  box-shadow: inset 0 0 0 1px var(--k), 0 1px 2px rgba(0,0,0,.06); transition: background .15s cubic-bezier(.2,0,0,1), box-shadow .15s cubic-bezier(.2,0,0,1);\n}\n.vs-command-palette-trigger:hover { background: color-mix(in srgb, var(--vs-command-palette-trigger-bg), var(--vs-command-palette-trigger-ink) 5%); }\n.vs-command-palette-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--k), 0 0 0 2px var(--vs-command-palette-trigger-focus, #2f6bff); }\n.vs-command-palette-trigger:active { background: color-mix(in srgb, var(--vs-command-palette-trigger-bg), var(--vs-command-palette-trigger-ink) 9%); transform: scale(.98); }\n@media (max-width: 640px) {\n  .vs-command-palette-root { padding: 12px 12px 0; }\n  .vs-command-palette-list { max-height: 52vh; }\n  .vs-command-palette-hint { display: none; }\n  .vs-command-palette-foot span:not(.vs-command-palette-act):not(:last-child) { display: none; }\n}\n@media (forced-colors: active) { .vs-command-palette-item[aria-selected=\"true\"] { outline: 2px solid Highlight; } }\n</style>\n\n<script>\n(() => {\n  if (window.vsCommandPalette) return;\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const mac = /Mac|iP(hone|ad|od)/.test(navigator.platform || navigator.userAgent);\n  const esc = s => s.replace(/[&<>\"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]));\n  const SEARCH = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg>';\n  let uid = 0;\n\n  // fzf-flavoured scoring: every query letter in order; word starts, runs and early hits win\n  function fuzzy(q, text) {\n    const s = text.toLowerCase(); let from = 0, prev = -2, score = 0; const hits = [];\n    for (const ch of q) {\n      if (ch === ' ') continue;\n      let best = -1, bestB = -1;\n      for (let i = s.indexOf(ch, from); i !== -1; i = s.indexOf(ch, i + 1)) {\n        const b = (i === prev + 1 ? 6 : 0) + (i === 0 || /[\\s\\-_/.·]/.test(s[i - 1]) ? 8 : 0);\n        if (b > bestB) { best = i; bestB = b; } if (b >= 8 || i - from > 24) break;\n      }\n      if (best < 0) return null;\n      score += 1 + bestB - Math.min(4, (best - prev - 1) * .3);\n      hits.push(best); prev = best; from = best + 1;\n    }\n    return { score: score + (hits[0] === 0 ? 4 : 0) - s.length * .02, hits };\n  }\n  const mark = (t, hits) => { let o = '', h = new Set(hits); for (let i = 0; i < t.length; i++) o += h.has(i) ? `<mark>${esc(t[i])}</mark>` : esc(t[i]); return o; };\n\n  function create(src) {\n    if (src.__vsCp) return src.__vsCp;\n    const id = 'vs-cp-' + (++uid);\n    // read the declarative items\n    const pages = { root: [] };\n    src.querySelectorAll(':scope > [data-group]').forEach(g => {\n      const page = g.dataset.pageId || 'root';\n      (pages[page] = pages[page] || []).push({ name: g.dataset.group, items: [...g.querySelectorAll(':scope > a, :scope > button')].map(el => {\n        const svg = el.querySelector('svg');\n        return { el, title: el.textContent.trim().replace(/\\s+/g, ' '), icon: svg ? svg.outerHTML : '', hint: el.dataset.hint || '',\n          kbd: (el.dataset.kbd || '').split(' ').filter(Boolean), keywords: el.dataset.keywords || '', page: el.dataset.page || '', action: el.dataset.action || (el.dataset.page ? 'Open' : el.tagName === 'A' ? 'Open' : 'Run') };\n      }) });\n    });\n    const root = document.createElement('div'); root.className = 'vs-command-palette-root'; root.hidden = true;\n    root.innerHTML = `<div class=\"vs-command-palette-scrim\"></div>\n      <div class=\"vs-command-palette-panel\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Command palette\">\n        <div class=\"vs-command-palette-head\">${SEARCH}<span class=\"vs-command-palette-crumb\" hidden></span>\n          <input class=\"vs-command-palette-input\" type=\"text\" role=\"combobox\" aria-expanded=\"true\" aria-controls=\"${id}-list\" aria-autocomplete=\"list\" autocomplete=\"off\" spellcheck=\"false\" placeholder=\"${esc(src.dataset.placeholder || 'Type a command or search…')}\">\n          <kbd>esc</kbd></div>\n        <div class=\"vs-command-palette-list\" id=\"${id}-list\" role=\"listbox\" aria-label=\"Commands\"><div class=\"vs-command-palette-hl\"></div><div class=\"vs-command-palette-body\"></div></div>\n        <div class=\"vs-command-palette-foot\"><span class=\"vs-command-palette-act\"></span><span><kbd>↑</kbd><kbd>↓</kbd> Move</span><span><kbd>↵</kbd> Select</span><span><kbd>${mac ? '⌘' : 'Ctrl'}</kbd><kbd>K</kbd> Close</span></div>\n      </div>`;\n    document.body.appendChild(root);\n    const $ = s => root.querySelector(s);\n    const scrim = $('.vs-command-palette-scrim'), panel = $('.vs-command-palette-panel'), input = $('input'), list = $('.vs-command-palette-list'),\n      body = $('.vs-command-palette-body'), hl = $('.vs-command-palette-hl'), crumb = $('.vs-command-palette-crumb'), act = $('.vs-command-palette-act');\n    // the root inherits the palette's variables from wherever the source sits\n    const VARS = ['bg','ink','dim','line','accent','hl','scrim','radius','width','height','font','mono','row','shadow'];\n    const sync = () => { const cs = getComputedStyle(src); VARS.forEach(k => { const v = cs.getPropertyValue('--vs-command-palette-' + k).trim(); if (v) root.style.setProperty('--vs-command-palette-' + k, v); else root.style.removeProperty('--vs-command-palette-' + k); }); if (!root.hidden) setActive(active, true); };\n    sync(); new MutationObserver(sync).observe(src, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n\n    let open = false, stack = ['root'], rows = [], active = 0, lastFocus = null;\n    const S = { o: { x: 0, v: 0, g: 0, k: 420, c: 34 }, y: { x: 0, v: 0, g: 0, k: 700, c: 46 }, h: { x: 0, v: 0, g: 0, k: 500, c: 40 } };\n    let raf = 0, last = 0;\n    const tick = now => {\n      const dt = Math.min(.064, (now - last) / 1000 || .016); last = now; let busy = false;\n      for (const k in S) { const s = S[k];\n        if (reduce.matches) { s.x = s.g; s.v = 0; continue; }\n        if (Math.abs(s.v) < .002 && Math.abs(s.x - s.g) < .002) { s.x = s.g; s.v = 0; continue; }\n        busy = true; const n = Math.max(1, Math.round(dt * 120)), h = dt / n;\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; } }\n      const o = Math.max(0, Math.min(1, S.o.x));\n      scrim.style.opacity = o.toFixed(3); panel.style.opacity = Math.min(1, o * 1.4).toFixed(3);\n      panel.style.transform = `translate3d(0,${((1 - S.o.x) * -14).toFixed(2)}px,0) scale(${(.955 + .045 * S.o.x).toFixed(4)})`;\n      hl.style.transform = `translate3d(0,${S.y.x.toFixed(2)}px,0)`; hl.style.opacity = Math.max(0, Math.min(1, S.h.x)).toFixed(3);\n      if (!busy && !open) { root.hidden = true; }\n      raf = busy ? requestAnimationFrame(tick) : 0;\n    };\n    const wake = () => { if (!raf) { last = performance.now(); raf = requestAnimationFrame(tick); } };\n\n    function render() {\n      const q = input.value.trim().toLowerCase(), page = stack[stack.length - 1];\n      let groups = pages[page] || [];\n      if (q) groups = groups.map(g => ({ name: g.name, items: g.items.map(it => {\n          const m = fuzzy(q, it.title), k = it.keywords ? fuzzy(q, it.keywords) : null;\n          const best = m && (!k || m.score >= k.score * .8) ? { s: m.score, hits: m.hits } : k ? { s: k.score * .8, hits: [] } : null;\n          return best && { it, s: best.s, hits: best.hits };\n        }).filter(Boolean).sort((a, b) => b.s - a.s) })).filter(g => g.items.length).sort((a, b) => b.items[0].s - a.items[0].s);\n      else groups = groups.map(g => ({ name: g.name, items: g.items.map(it => ({ it, hits: [] })) }));\n      rows = []; let html = '';\n      groups.forEach((g, gi) => {\n        html += `<div class=\"vs-command-palette-group\" role=\"presentation\" id=\"${id}-g${gi}\">${esc(g.name)}</div>`;\n        g.items.forEach(({ it, hits }) => {\n          const i = rows.length; rows.push(it);\n          html += `<div class=\"vs-command-palette-item\" role=\"option\" id=\"${id}-o${i}\" data-i=\"${i}\" aria-selected=\"false\" aria-describedby=\"${id}-g${gi}\">` +\n            (it.icon ? `<span class=\"vs-command-palette-icon\">${it.icon}</span>` : '') +\n            `<span class=\"vs-command-palette-title\">${mark(it.title, hits)}</span>` +\n            (it.hint ? `<span class=\"vs-command-palette-hint\">${esc(it.hint)}</span>` : '') +\n            (it.kbd.length ? `<span class=\"vs-command-palette-keys\">${it.kbd.map(k => `<kbd>${esc(k)}</kbd>`).join('')}</span>` : '') + `</div>`;\n        });\n      });\n      if (!rows.length) html = `<div class=\"vs-command-palette-empty\">Nothing matches <b>“${esc(input.value.trim())}”</b>.<br>Try fewer letters, or a word from the start of a name.</div>`;\n      body.innerHTML = html;\n      setActive(0, true);\n    }\n    function setActive(i, jump) {\n      const items = body.querySelectorAll('.vs-command-palette-item');\n      if (!items.length) { S.h.g = 0; act.textContent = ''; input.removeAttribute('aria-activedescendant'); wake(); return; }\n      active = (i + items.length) % items.length;\n      items.forEach((n, j) => n.setAttribute('aria-selected', String(j === active)));\n      const n = items[active]; input.setAttribute('aria-activedescendant', n.id);\n      hl.style.height = n.offsetHeight + 'px'; S.y.g = n.offsetTop; if (jump) { S.y.x = S.y.g; S.y.v = 0; } S.h.g = 1;\n      const r = rows[active]; act.innerHTML = `<kbd>↵</kbd> ${esc(r.action)} <span style=\"opacity:.55\">·</span> <span style=\"opacity:.55;display:inline\">${esc(r.title)}</span>`;\n      const top = n.offsetTop, bottom = top + n.offsetHeight, pad = 8;\n      if (top - pad < list.scrollTop) list.scrollTop = top - (active === 0 ? 60 : pad); else if (bottom + pad > list.scrollTop + list.clientHeight) list.scrollTop = bottom + pad - list.clientHeight;\n      wake();\n    }\n    function go(page, back) {\n      if (back) stack.pop(); else stack.push(page);\n      const cur = stack[stack.length - 1];\n      crumb.hidden = cur === 'root';\n      if (cur !== 'root') { const g = (pages[cur] || [])[0]; crumb.textContent = g ? g.name : cur; }\n      input.placeholder = cur === 'root' ? (src.dataset.placeholder || 'Type a command or search…') : `Choose ${crumb.textContent.toLowerCase()}…`;\n      input.value = ''; render();\n      if (!reduce.matches) body.animate([{ opacity: 0, transform: `translateX(${back ? -14 : 14}px)` }, { opacity: 1, transform: 'none' }], { duration: 320, easing: 'cubic-bezier(.16,1,.3,1)' });\n    }\n    function choose(i) {\n      const r = rows[i]; if (!r) return;\n      if (r.page) { go(r.page); return; }\n      close();\n      src.dispatchEvent(new CustomEvent('vs-command', { detail: r.el, bubbles: true }));\n      setTimeout(() => r.el.click(), 10);\n    }\n    function show() {\n      if (open) return; open = true; lastFocus = document.activeElement;\n      stack = ['root']; crumb.hidden = true; input.value = ''; input.placeholder = src.dataset.placeholder || 'Type a command or search…'; root.hidden = false; render();\n      S.o.g = 1; S.o.v = 0; wake(); input.focus({ preventScroll: true });\n      if (!reduce.matches) [...body.children].slice(0, 9).forEach((n, i) => n.animate([{ opacity: 0, transform: 'translateY(6px)' }, { opacity: 1, transform: 'none' }], { duration: 420, delay: 40 + i * 18, easing: 'cubic-bezier(.16,1,.3,1)', fill: 'backwards' }));\n    }\n    function close() {\n      if (!open) return; open = false; S.o.g = 0; S.h.g = 0; wake();\n      if (lastFocus && lastFocus.focus) lastFocus.focus({ preventScroll: true });\n    }\n\n    input.addEventListener('input', render);\n    input.addEventListener('keydown', e => {\n      if (e.key === 'ArrowDown' || (e.key === 'n' && e.ctrlKey)) { e.preventDefault(); setActive(active + 1); }\n      else if (e.key === 'ArrowUp' || (e.key === 'p' && e.ctrlKey)) { e.preventDefault(); setActive(active - 1); }\n      else if (e.key === 'Home' && !input.value) { e.preventDefault(); setActive(0); }\n      else if (e.key === 'End' && !input.value) { e.preventDefault(); setActive(-1); }\n      else if (e.key === 'Enter') { e.preventDefault(); choose(active); }\n      else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); if (stack.length > 1 && !input.value) go(null, true); else close(); }\n      else if (e.key === 'Backspace' && !input.value && stack.length > 1) { e.preventDefault(); go(null, true); }\n      else if (e.key === 'Tab') e.preventDefault();\n    });\n    body.addEventListener('pointermove', e => { const n = e.target.closest('.vs-command-palette-item'); if (n && +n.dataset.i !== active) setActive(+n.dataset.i); });\n    body.addEventListener('click', e => { const n = e.target.closest('.vs-command-palette-item'); if (n) choose(+n.dataset.i); });\n    scrim.addEventListener('click', close);\n    root.addEventListener('pointerdown', e => { if (e.target === root) close(); });\n    panel.addEventListener('pointerdown', e => { if (e.target !== input && !e.target.closest('.vs-command-palette-item')) e.preventDefault(); });\n\n    const key = (src.dataset.hotkey || 'k').toLowerCase();\n    document.addEventListener('keydown', e => {\n      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === key) { e.preventDefault(); open ? close() : show(); }\n      else if (e.key === 'Escape' && open) close();\n    });\n    document.querySelectorAll('.vs-command-palette-trigger').forEach(t => {\n      t.addEventListener('click', show);\n      if (!mac) t.querySelectorAll('kbd').forEach(k => k.textContent = k.textContent.replace('⌘', 'Ctrl '));\n    });\n    const api = { open: show, close, root, sync, get isOpen() { return open; } };\n    src.vsOpen = show; src.vsClose = close; src.__vsCp = api;\n    return api;\n  }\n  const init = () => document.querySelectorAll('.vs-command-palette').forEach(create);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n  window.vsCommandPalette = { create, fuzzy };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Buttons & inputs",
    "tags": [
      "command palette",
      "cmdk",
      "search",
      "fuzzy",
      "keyboard",
      "modal",
      "combobox"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#2f6bff",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/command-palette/index.html",
    "thumbnail": "https://vividsites.app/api/component/command-palette/thumb.jpg",
    "video": "https://vividsites.app/api/component/command-palette/thumb.mp4",
    "page": "https://vividsites.app/c/command-palette"
  }
}