modern-cmdk 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/codemod/cli.mjs +1 -3
  2. package/dist/codemod/transforms/data-attrs.d.mts +1 -2
  3. package/dist/codemod/transforms/data-attrs.mjs +0 -2
  4. package/dist/codemod/transforms/forward-ref.d.mts +1 -2
  5. package/dist/codemod/transforms/forward-ref.mjs +0 -2
  6. package/dist/codemod/transforms/import-rewrite.d.mts +1 -2
  7. package/dist/codemod/transforms/import-rewrite.mjs +0 -2
  8. package/dist/codemod/transforms/should-filter.d.mts +1 -2
  9. package/dist/codemod/transforms/should-filter.mjs +0 -2
  10. package/dist/core/index.d.mts +568 -2
  11. package/dist/core/index.mjs +1268 -1
  12. package/dist/react/index.d.mts +369 -4
  13. package/dist/react/index.mjs +1380 -265
  14. package/{src → dist}/react/styles.css +1 -3
  15. package/package.json +28 -12
  16. package/dist/codemod/cli.mjs.map +0 -1
  17. package/dist/codemod/transforms/data-attrs.d.mts.map +0 -1
  18. package/dist/codemod/transforms/data-attrs.mjs.map +0 -1
  19. package/dist/codemod/transforms/forward-ref.d.mts.map +0 -1
  20. package/dist/codemod/transforms/forward-ref.mjs.map +0 -1
  21. package/dist/codemod/transforms/import-rewrite.d.mts.map +0 -1
  22. package/dist/codemod/transforms/import-rewrite.mjs.map +0 -1
  23. package/dist/codemod/transforms/should-filter.d.mts.map +0 -1
  24. package/dist/codemod/transforms/should-filter.mjs.map +0 -1
  25. package/dist/core-DZT7Vwmo.mjs +0 -1219
  26. package/dist/core-DZT7Vwmo.mjs.map +0 -1
  27. package/dist/index-zBlDFopY.d.mts +0 -562
  28. package/dist/index-zBlDFopY.d.mts.map +0 -1
  29. package/dist/react/index.d.mts.map +0 -1
  30. package/dist/react/index.mjs.map +0 -1
@@ -1,8 +1,9 @@
1
- import { C as itemId, S as groupId, d as formatShortcut, f as parseShortcut, i as createCommandMachine } from "../core-DZT7Vwmo.mjs";
1
+ 'use client';
2
2
  import * as React from "react";
3
- import { Component, Suspense, createContext, use, useCallback, useEffect, useId, useInsertionEffect, useMemo, useOptimistic, useRef, useState, useSyncExternalStore, useTransition } from "react";
3
+ import { Component, Suspense, createContext, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useOptimistic, useRef, useState, useSyncExternalStore, useTransition } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Dialog } from "radix-ui";
6
+ import { createPortal } from "react-dom";
6
7
  //#region src/react/activity.tsx
7
8
  const ActivityComponent = "Activity" in React ? React.Activity : null;
8
9
  /**
@@ -20,25 +21,42 @@ function CommandActivity({ mode, children }) {
20
21
  }
21
22
  //#endregion
22
23
  //#region src/react/context.ts
23
- const CommandContext = createContext(null);
24
- CommandContext.displayName = "CommandContext";
24
+ const CommandStableContext = createContext(null);
25
+ CommandStableContext.displayName = "CommandStableContext";
26
+ const CommandStateContext = createContext(null);
27
+ CommandStateContext.displayName = "CommandStateContext";
28
+ /** Consume stable context with a descriptive error — replaces 3-line boilerplate in every component */
29
+ function useStableContext(component) {
30
+ const ctx = use(CommandStableContext);
31
+ if (!ctx) throw new Error(`${component} must be used within a <Command> component`);
32
+ return ctx;
33
+ }
34
+ /** Consume state context with a descriptive error — replaces 3-line boilerplate in every component */
35
+ function useStateContext(component) {
36
+ const ctx = use(CommandStateContext);
37
+ if (!ctx) throw new Error(`${component} must be used within a <Command> component`);
38
+ return ctx;
39
+ }
25
40
  const CommandGroupContext = createContext(null);
26
41
  CommandGroupContext.displayName = "CommandGroupContext";
42
+ /** Context for the status container rendered outside the listbox — used by Command.Empty */
43
+ const CommandListStatusContext = createContext(null);
44
+ CommandListStatusContext.displayName = "CommandListStatusContext";
27
45
  //#endregion
28
46
  //#region src/react/async-items.tsx
29
47
  /** Inner component that suspends via use() until items resolve */
30
48
  function AsyncItemsInner({ items: itemsPromise, children }) {
31
- const ctx = use(CommandContext);
32
- if (!ctx) throw new Error("Command.AsyncItems must be used within a <Command> component");
49
+ const stable = useStableContext("Command.AsyncItems");
33
50
  const items = use(itemsPromise);
34
- useEffect(() => {
35
- ctx.machine.send({
51
+ useLayoutEffect(() => {
52
+ stable.machine.send({
36
53
  type: "ITEMS_LOADED",
37
54
  items
38
55
  });
39
- }, [ctx.machine, items]);
56
+ }, [stable.machine, items]);
40
57
  return /* @__PURE__ */ jsx(Fragment, { children: children(items) });
41
58
  }
59
+ /** Suspense-based async item loader that resolves a promise of items using React 19 `use()`. */
42
60
  function CommandAsyncItems({ items, fallback, children }) {
43
61
  const stableRef = useRef(items);
44
62
  if (stableRef.current !== items) stableRef.current = items;
@@ -52,6 +70,7 @@ function CommandAsyncItems({ items, fallback, children }) {
52
70
  }
53
71
  //#endregion
54
72
  //#region src/react/badge.tsx
73
+ /** Inline badge for categorization or status indicators on command items. */
55
74
  function CommandBadge({ ref, children, ...props }) {
56
75
  return /* @__PURE__ */ jsx("span", {
57
76
  ref,
@@ -61,13 +80,1078 @@ function CommandBadge({ ref, children, ...props }) {
61
80
  });
62
81
  }
63
82
  //#endregion
83
+ //#region src/core/types.ts
84
+ /** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode (ES2026). */
85
+ function itemId(id) {
86
+ return id.isWellFormed() ? id : id.toWellFormed();
87
+ }
88
+ /** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode (ES2026). */
89
+ function groupId(id) {
90
+ return id.isWellFormed() ? id : id.toWellFormed();
91
+ }
92
+ /** Default frecency decay configuration */
93
+ const DEFAULT_FRECENCY_DECAY = {
94
+ hourWeight: 4,
95
+ dayWeight: 2,
96
+ weekWeight: 1.5,
97
+ monthWeight: 1,
98
+ olderWeight: .5
99
+ };
100
+ /** Initial state factory */
101
+ function createInitialState(options) {
102
+ return {
103
+ search: "",
104
+ activeId: null,
105
+ filteredIds: [],
106
+ groupedIds: /* @__PURE__ */ new Map(),
107
+ filteredCount: 0,
108
+ loading: false,
109
+ page: "root",
110
+ pageStack: [],
111
+ open: options?.open ?? false,
112
+ lastUpdated: Temporal.Now.instant()
113
+ };
114
+ }
115
+ //#endregion
116
+ //#region src/core/frecency/memory-storage.ts
117
+ /**
118
+ * In-memory implementation of FrecencyStorage using a Map.
119
+ * Data does not persist across page reloads. Suitable for ephemeral
120
+ * sessions or as the default when no persistent storage is configured.
121
+ */
122
+ var MemoryFrecencyStorage = class {
123
+ #store = /* @__PURE__ */ new Map();
124
+ load(namespace) {
125
+ return this.#store.get(namespace) ?? { records: /* @__PURE__ */ new Map() };
126
+ }
127
+ save(namespace, data) {
128
+ this.#store.set(namespace, data);
129
+ }
130
+ /** Serialize all stored data — Iterator Helpers pipeline (ES2026) */
131
+ toJSON() {
132
+ return Object.fromEntries(this.#store.entries().map(([namespace, data]) => [namespace, { records: data.records.entries().toArray() }]));
133
+ }
134
+ [Symbol.dispose]() {
135
+ this.#store.clear();
136
+ }
137
+ };
138
+ //#endregion
139
+ //#region src/core/frecency/index.ts
140
+ /** Shared empty bonus map — avoids allocation when no frecency records exist */
141
+ const EMPTY_BONUS_MAP = /* @__PURE__ */ new Map();
142
+ /** Pre-computed bucket boundaries in hours — avoids Duration.total() per call */
143
+ const BUCKET_HOURS = {
144
+ hour: 1,
145
+ day: 24,
146
+ week: 168,
147
+ month: 720
148
+ };
149
+ /**
150
+ * Computes a frecency bonus for an item based on usage frequency and recency.
151
+ * Uses Temporal.Duration bucket boundaries (hour/day/week/month) with
152
+ * configurable exponential decay weights per time bucket.
153
+ */
154
+ function computeFrecencyBonus(history, now = Temporal.Now.instant(), config = DEFAULT_FRECENCY_DECAY) {
155
+ const hours = now.since(history.lastUsed).total("hours");
156
+ const { hourWeight = 4, dayWeight = 2, weekWeight = 1.5, monthWeight = 1, olderWeight = .5 } = config;
157
+ const recencyWeight = hours < BUCKET_HOURS.hour ? hourWeight : hours < BUCKET_HOURS.day ? dayWeight : hours < BUCKET_HOURS.week ? weekWeight : hours < BUCKET_HOURS.month ? monthWeight : olderWeight;
158
+ return history.frequency * recencyWeight;
159
+ }
160
+ /**
161
+ * Stateful frecency engine that tracks item selection history and provides
162
+ * ranking boosts for search results. Supports pluggable storage backends
163
+ * and automatic flush on dispose. Uses Temporal.Now.instant() for timestamps.
164
+ */
165
+ var FrecencyEngine = class {
166
+ #storage;
167
+ #namespace;
168
+ #data;
169
+ #config;
170
+ #dirty = false;
171
+ constructor(options) {
172
+ this.#storage = options.storage ?? new MemoryFrecencyStorage();
173
+ this.#namespace = options.namespace ?? "default";
174
+ this.#config = options.decayConfig ?? DEFAULT_FRECENCY_DECAY;
175
+ this.#data = { records: /* @__PURE__ */ new Map() };
176
+ }
177
+ /** Load frecency data from storage */
178
+ async load() {
179
+ this.#data = await this.#storage.load(this.#namespace);
180
+ }
181
+ /** Record a selection event for an item */
182
+ recordSelection(itemId) {
183
+ const now = Temporal.Now.instant();
184
+ const updated = {
185
+ frequency: (this.#data.records.get(itemId)?.frequency ?? 0) + 1,
186
+ lastUsed: now
187
+ };
188
+ this.#data.records.set(itemId, updated);
189
+ this.#dirty = true;
190
+ }
191
+ /** Get frecency bonus for an item */
192
+ getBonus(itemId, now) {
193
+ const record = this.#data.records.get(itemId);
194
+ if (!record) return 0;
195
+ return computeFrecencyBonus(record, now, this.#config);
196
+ }
197
+ /** Get all frecency bonuses as a map — for...of for hot ranking path (no closure overhead) */
198
+ getAllBonuses(now = Temporal.Now.instant()) {
199
+ if (this.#data.records.size === 0) return EMPTY_BONUS_MAP;
200
+ const bonuses = /* @__PURE__ */ new Map();
201
+ for (const [id, record] of this.#data.records) bonuses.set(id, computeFrecencyBonus(record, now, this.#config));
202
+ return bonuses;
203
+ }
204
+ /** Get the decay configuration */
205
+ getConfig() {
206
+ return this.#config;
207
+ }
208
+ /** Update decay configuration at runtime */
209
+ setConfig(config) {
210
+ this.#config = {
211
+ ...this.#config,
212
+ ...config
213
+ };
214
+ }
215
+ /** Flush dirty data to storage */
216
+ async flush() {
217
+ if (!this.#dirty) return;
218
+ await this.#storage.save(this.#namespace, this.#data);
219
+ this.#dirty = false;
220
+ }
221
+ /** Get the raw frecency data */
222
+ getData() {
223
+ return this.#data;
224
+ }
225
+ /** Async dispose — flushes dirty data before cleanup. Use with `await using`. */
226
+ async [Symbol.asyncDispose]() {
227
+ if (this.#dirty) {
228
+ await this.#storage.save(this.#namespace, this.#data);
229
+ this.#dirty = false;
230
+ }
231
+ this.#storage[Symbol.dispose]();
232
+ }
233
+ [Symbol.dispose]() {
234
+ if (this.#dirty) Promise.try(() => this.#storage.save(this.#namespace, this.#data)).catch((err) => {
235
+ console.warn("[modern-cmdk] FrecencyEngine dispose flush failed:", err);
236
+ });
237
+ this.#storage[Symbol.dispose]();
238
+ }
239
+ };
240
+ //#endregion
241
+ //#region src/core/keyboard/matcher.ts
242
+ /**
243
+ * Tests whether a KeyboardEvent exactly matches a ParsedShortcut,
244
+ * comparing all four modifier keys (meta, ctrl, shift, alt) and the primary key.
245
+ */
246
+ function matchesShortcut(event, shortcut) {
247
+ if (shortcut.meta !== event.metaKey) return false;
248
+ if (shortcut.ctrl !== event.ctrlKey) return false;
249
+ if (shortcut.shift !== event.shiftKey) return false;
250
+ if (shortcut.alt !== event.altKey) return false;
251
+ return event.key.toLowerCase() === shortcut.key;
252
+ }
253
+ /**
254
+ * Detects conflicting shortcut bindings by grouping on their normalized form
255
+ * via Map.groupBy (ES2026). Returns only groups with two or more colliding shortcuts.
256
+ * Map.groupBy returns a Map directly — no Object.entries conversion needed.
257
+ */
258
+ function detectConflicts(shortcuts) {
259
+ const grouped = Map.groupBy(shortcuts, (s) => s.normalized);
260
+ return new Map(grouped.entries().filter(([, group]) => group.length > 1));
261
+ }
262
+ //#endregion
263
+ //#region src/core/keyboard/parser.ts
264
+ const isMac = typeof navigator !== "undefined" && (navigator.userAgentData?.platform === "macOS" || /Mac|iPod|iPhone|iPad/.test(navigator.platform));
265
+ /** Modifier aliases — cross-platform Mod resolution */
266
+ const MODIFIER_MAP = {
267
+ mod: isMac ? "meta" : "ctrl",
268
+ meta: "meta",
269
+ cmd: "meta",
270
+ command: "meta",
271
+ ctrl: "ctrl",
272
+ control: "ctrl",
273
+ shift: "shift",
274
+ alt: "alt",
275
+ option: "alt",
276
+ opt: "alt"
277
+ };
278
+ /**
279
+ * Parse a shortcut string like "Mod+Shift+K" into a structured ParsedShortcut.
280
+ * Uses String.isWellFormed() (ES2026) to ensure valid Unicode input.
281
+ */
282
+ function parseShortcut(shortcut) {
283
+ const safeShortcut = shortcut.isWellFormed() ? shortcut : shortcut.toWellFormed();
284
+ const parts = safeShortcut.split("+").map((p) => p.trim().toLowerCase());
285
+ let meta = false;
286
+ let ctrl = false;
287
+ let shift = false;
288
+ let alt = false;
289
+ let key = "";
290
+ for (const part of parts) {
291
+ const modifier = MODIFIER_MAP[part];
292
+ if (modifier) switch (modifier) {
293
+ case "meta":
294
+ meta = true;
295
+ break;
296
+ case "ctrl":
297
+ ctrl = true;
298
+ break;
299
+ case "shift":
300
+ shift = true;
301
+ break;
302
+ case "alt":
303
+ alt = true;
304
+ break;
305
+ }
306
+ else key = part;
307
+ }
308
+ const modifiers = [];
309
+ if (meta) modifiers.push("meta");
310
+ if (ctrl) modifiers.push("ctrl");
311
+ if (shift) modifiers.push("shift");
312
+ if (alt) modifiers.push("alt");
313
+ modifiers.push(key);
314
+ return {
315
+ key,
316
+ meta,
317
+ ctrl,
318
+ shift,
319
+ alt,
320
+ raw: safeShortcut,
321
+ normalized: modifiers.join("+")
322
+ };
323
+ }
324
+ /**
325
+ * Formats a ParsedShortcut for display using platform-appropriate symbols.
326
+ * On macOS: renders glyphs (⌘, ⇧, ⌥, ⌃). On other platforms: text labels joined by "+".
327
+ */
328
+ function formatShortcut(parsed) {
329
+ const parts = [];
330
+ if (parsed.meta) parts.push(isMac ? "⌘" : "Ctrl");
331
+ if (parsed.ctrl && !(!isMac && parsed.meta)) parts.push(isMac ? "⌃" : "Ctrl");
332
+ if (parsed.shift) parts.push(isMac ? "⇧" : "Shift");
333
+ if (parsed.alt) parts.push(isMac ? "⌥" : "Alt");
334
+ const displayKey = parsed.key.length === 1 ? parsed.key.toUpperCase() : parsed.key.charAt(0).toUpperCase() + parsed.key.slice(1);
335
+ parts.push(displayKey);
336
+ return isMac ? parts.join("") : parts.join("+");
337
+ }
338
+ //#endregion
339
+ //#region src/core/keyboard/index.ts
340
+ /**
341
+ * Manages keyboard shortcut bindings with global keydown listening.
342
+ * Supports `using` for automatic cleanup of both individual bindings and the
343
+ * global document listener. Detects conflicts via Map.groupBy (ES2026).
344
+ */
345
+ var KeyboardShortcutRegistry = class {
346
+ #bindings = /* @__PURE__ */ new Map();
347
+ #globalListener = null;
348
+ #enabled = true;
349
+ constructor(options) {
350
+ this.#enabled = options?.enabled ?? true;
351
+ this.#attachGlobalListener();
352
+ }
353
+ /** Register a shortcut binding — returns Disposable for `using` auto-deregister */
354
+ register(shortcutStr, itemId, handler) {
355
+ const shortcut = parseShortcut(shortcutStr);
356
+ this.#bindings.set(shortcut.normalized, {
357
+ shortcut,
358
+ itemId,
359
+ handler
360
+ });
361
+ return { [Symbol.dispose]: () => {
362
+ this.#bindings.delete(shortcut.normalized);
363
+ } };
364
+ }
365
+ /** Unregister a shortcut by its string representation */
366
+ unregister(shortcutStr) {
367
+ const shortcut = parseShortcut(shortcutStr);
368
+ this.#bindings.delete(shortcut.normalized);
369
+ }
370
+ /** Unregister all shortcuts for a given item — Map spec allows deletion during iteration */
371
+ unregisterByItem(itemId) {
372
+ for (const [key, binding] of this.#bindings) if (binding.itemId === itemId) this.#bindings.delete(key);
373
+ }
374
+ /** Check for conflicting shortcuts */
375
+ getConflicts() {
376
+ return detectConflicts(this.#bindings.values().map((b) => b.shortcut).toArray());
377
+ }
378
+ /** Enable or disable global shortcut listening */
379
+ setEnabled(enabled) {
380
+ this.#enabled = enabled;
381
+ }
382
+ /** Get all registered bindings */
383
+ getBindings() {
384
+ return this.#bindings;
385
+ }
386
+ #attachGlobalListener() {
387
+ if (typeof document === "undefined") return;
388
+ this.#globalListener = (event) => {
389
+ if (!this.#enabled) return;
390
+ for (const binding of this.#bindings.values()) if (matchesShortcut(event, binding.shortcut)) {
391
+ event.preventDefault();
392
+ event.stopPropagation();
393
+ binding.handler();
394
+ return;
395
+ }
396
+ };
397
+ document.addEventListener("keydown", this.#globalListener);
398
+ }
399
+ #detachGlobalListener() {
400
+ if (this.#globalListener && typeof document !== "undefined") {
401
+ document.removeEventListener("keydown", this.#globalListener);
402
+ this.#globalListener = null;
403
+ }
404
+ }
405
+ [Symbol.dispose]() {
406
+ this.#detachGlobalListener();
407
+ this.#bindings.clear();
408
+ }
409
+ };
410
+ //#endregion
411
+ //#region src/core/registry.ts
412
+ /**
413
+ * Manages registration and lookup of command items and groups.
414
+ * Uses ES2026 Set methods (union/intersection/difference) for efficient ID operations.
415
+ * All registration methods return Disposable for automatic cleanup via `using`.
416
+ */
417
+ var CommandRegistry = class {
418
+ #items = /* @__PURE__ */ new Map();
419
+ #groups = /* @__PURE__ */ new Map();
420
+ #itemOrder = [];
421
+ #itemOrderSet = /* @__PURE__ */ new Set();
422
+ #cachedItems = null;
423
+ #cachedGroups = null;
424
+ #invalidateCache() {
425
+ this.#cachedItems = null;
426
+ }
427
+ #invalidateGroupCache() {
428
+ this.#cachedGroups = null;
429
+ }
430
+ /** Register an item — returns Disposable for `using` auto-deregister */
431
+ registerItem(item) {
432
+ this.#items.set(item.id, item);
433
+ if (!this.#itemOrderSet.has(item.id)) {
434
+ this.#itemOrder.push(item.id);
435
+ this.#itemOrderSet.add(item.id);
436
+ }
437
+ this.#invalidateCache();
438
+ return { [Symbol.dispose]: () => {
439
+ this.unregisterItem(item.id);
440
+ } };
441
+ }
442
+ /** Register multiple items at once — single Disposable for batch cleanup. for...of for hot path. */
443
+ registerItems(items) {
444
+ for (const item of items) {
445
+ this.#items.set(item.id, item);
446
+ if (!this.#itemOrderSet.has(item.id)) {
447
+ this.#itemOrder.push(item.id);
448
+ this.#itemOrderSet.add(item.id);
449
+ }
450
+ }
451
+ this.#invalidateCache();
452
+ const ids = new Set(items.values().map((i) => i.id));
453
+ return { [Symbol.dispose]: () => {
454
+ this.unregisterItems(ids);
455
+ } };
456
+ }
457
+ /** Unregister a single item — O(1) Map/Set delete, indexOf+toSpliced for order */
458
+ unregisterItem(id) {
459
+ if (!this.#items.has(id)) return;
460
+ this.#items.delete(id);
461
+ this.#itemOrderSet.delete(id);
462
+ const idx = this.#itemOrder.indexOf(id);
463
+ if (idx !== -1) this.#itemOrder = this.#itemOrder.toSpliced(idx, 1);
464
+ this.#invalidateCache();
465
+ }
466
+ /** Bulk unregister using Set.difference (ES2026) — for...of for hot path */
467
+ unregisterItems(ids) {
468
+ for (const id of ids) this.#items.delete(id);
469
+ this.#itemOrderSet = this.#itemOrderSet.difference(ids);
470
+ this.#itemOrder = this.#itemOrder.filter((id) => !ids.has(id));
471
+ this.#invalidateCache();
472
+ }
473
+ /** Register a group */
474
+ registerGroup(group) {
475
+ this.#groups.set(group.id, group);
476
+ this.#invalidateGroupCache();
477
+ return { [Symbol.dispose]: () => {
478
+ this.#groups.delete(group.id);
479
+ this.#invalidateGroupCache();
480
+ } };
481
+ }
482
+ /** Unregister a group */
483
+ unregisterGroup(id) {
484
+ this.#groups.delete(id);
485
+ this.#invalidateGroupCache();
486
+ }
487
+ /** Get an item by ID — O(1) */
488
+ getItem(id) {
489
+ return this.#items.get(id);
490
+ }
491
+ /** Get a group by ID — O(1) */
492
+ getGroup(id) {
493
+ return this.#groups.get(id);
494
+ }
495
+ /** Get all registered items in insertion order — cached between mutations */
496
+ getItems() {
497
+ if (this.#cachedItems !== null) return this.#cachedItems;
498
+ this.#cachedItems = this.#itemOrder.values().map((id) => this.#items.get(id)).filter((item) => item != null).toArray();
499
+ return this.#cachedItems;
500
+ }
501
+ /** Get all registered groups sorted by priority — cached between group mutations */
502
+ getGroups() {
503
+ if (this.#cachedGroups !== null) return this.#cachedGroups;
504
+ this.#cachedGroups = this.#groups.values().toArray().toSorted((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
505
+ return this.#cachedGroups;
506
+ }
507
+ /** Get items grouped by groupId using Map.groupBy (ES2026) — direct Map, no Object.entries conversion */
508
+ getGroupedItems() {
509
+ const items = this.getItems();
510
+ const grouped = Map.groupBy(items, (item) => item.groupId ?? "__ungrouped");
511
+ const result = /* @__PURE__ */ new Map();
512
+ for (const group of this.getGroups()) {
513
+ const groupItems = grouped.get(group.id);
514
+ if (groupItems != null) result.set(group.id, groupItems);
515
+ }
516
+ const ungrouped = grouped.get("__ungrouped");
517
+ if (ungrouped) result.set("__ungrouped", ungrouped);
518
+ return result;
519
+ }
520
+ /** Get item IDs as a Set — returns internal set (O(1), no allocation) */
521
+ getItemIds() {
522
+ return this.#itemOrderSet;
523
+ }
524
+ /** Find intersection of registered items with a candidate set (ES2026 Set.intersection) */
525
+ intersectWith(candidateIds) {
526
+ return this.#itemOrderSet.intersection(candidateIds);
527
+ }
528
+ /** Find items registered but not in a given set (ES2026 Set.difference) */
529
+ differenceFrom(otherIds) {
530
+ return this.#itemOrderSet.difference(otherIds);
531
+ }
532
+ /** Union of registered IDs with another set (ES2026 Set.union) */
533
+ unionWith(otherIds) {
534
+ return this.#itemOrderSet.union(otherIds);
535
+ }
536
+ /** Check if registered items are a subset of the given set (ES2026 Set.isSubsetOf) */
537
+ isSubsetOf(otherIds) {
538
+ return this.#itemOrderSet.isSubsetOf(otherIds);
539
+ }
540
+ /** Check if registered items are a superset of the given set (ES2026 Set.isSupersetOf) */
541
+ isSupersetOf(otherIds) {
542
+ return this.#itemOrderSet.isSupersetOf(otherIds);
543
+ }
544
+ /** Check if registered items share no IDs with the given set (ES2026 Set.isDisjointFrom) */
545
+ isDisjointFrom(otherIds) {
546
+ return this.#itemOrderSet.isDisjointFrom(otherIds);
547
+ }
548
+ /** Symmetric difference between registered IDs and another set (ES2026 Set.symmetricDifference) */
549
+ symmetricDifferenceWith(otherIds) {
550
+ return this.#itemOrderSet.symmetricDifference(otherIds);
551
+ }
552
+ /** Total number of registered items */
553
+ get size() {
554
+ return this.#items.size;
555
+ }
556
+ /** Total number of registered groups */
557
+ get groupCount() {
558
+ return this.#groups.size;
559
+ }
560
+ /** Clear all items and groups */
561
+ clear() {
562
+ this.#items.clear();
563
+ this.#groups.clear();
564
+ this.#itemOrder = [];
565
+ this.#itemOrderSet.clear();
566
+ this.#invalidateCache();
567
+ this.#invalidateGroupCache();
568
+ }
569
+ [Symbol.dispose]() {
570
+ this.clear();
571
+ }
572
+ };
573
+ //#endregion
574
+ //#region src/core/search/default-scorer.ts
575
+ function isWordSeparator(code) {
576
+ return code === 32 || code === 9 || code === 45 || code === 95 || code === 46 || code === 47 || code === 92;
577
+ }
578
+ /**
579
+ * Internal fast path: called by search engine with pre-lowered query and targets.
580
+ * Eliminates all toLowerCase() calls on the hot path.
581
+ */
582
+ function scoreItemPreLowered(lowerQuery, id, lowerValue, lowerKeywords) {
583
+ let bestScore = 0;
584
+ let bestMatches = null;
585
+ const valueResult = scoreTarget(lowerQuery, lowerValue);
586
+ if (valueResult !== null) {
587
+ bestScore = valueResult.score;
588
+ bestMatches = valueResult.matches;
589
+ if (bestScore >= 1) return {
590
+ id,
591
+ score: 1,
592
+ matches: bestMatches
593
+ };
594
+ }
595
+ if (lowerKeywords !== void 0) for (let i = 0; i < lowerKeywords.length; i++) {
596
+ const result = scoreTarget(lowerQuery, lowerKeywords[i]);
597
+ if (result !== null && result.score > bestScore) {
598
+ bestScore = result.score;
599
+ bestMatches = result.matches;
600
+ if (bestScore >= 1) break;
601
+ }
602
+ }
603
+ return bestMatches !== null ? {
604
+ id,
605
+ score: bestScore,
606
+ matches: bestMatches
607
+ } : null;
608
+ }
609
+ function scoreTarget(query, lowerTarget) {
610
+ const tLen = lowerTarget.length;
611
+ if (tLen === 0) return null;
612
+ const qLen = query.length;
613
+ if (qLen === 0) return {
614
+ score: 1,
615
+ matches: []
616
+ };
617
+ if (qLen > tLen) return null;
618
+ if (lowerTarget === query) return {
619
+ score: 1,
620
+ matches: [[0, qLen]]
621
+ };
622
+ if (lowerTarget.startsWith(query)) return {
623
+ score: .9 + .1 * (qLen / tLen),
624
+ matches: [[0, qLen]]
625
+ };
626
+ const substringIdx = lowerTarget.indexOf(query);
627
+ if (substringIdx !== -1) {
628
+ const positionBonus = 1 - substringIdx / tLen;
629
+ const lengthRatio = qLen / tLen;
630
+ return {
631
+ score: .5 + .3 * positionBonus + .2 * lengthRatio,
632
+ matches: [[substringIdx, substringIdx + qLen]]
633
+ };
634
+ }
635
+ const wordBoundaryResult = scoreWordBoundary(query, qLen, lowerTarget, tLen);
636
+ if (wordBoundaryResult) return wordBoundaryResult;
637
+ return scoreFuzzy(query, qLen, lowerTarget, tLen);
638
+ }
639
+ function scoreWordBoundary(query, qLen, lowerTarget, tLen) {
640
+ let queryIdx = 0;
641
+ const matches = [];
642
+ let totalWeightedLen = 0;
643
+ let i = 0;
644
+ while (i < tLen && queryIdx < qLen) if ((i === 0 || isWordSeparator(lowerTarget.charCodeAt(i - 1))) && lowerTarget.charCodeAt(i) === query.charCodeAt(queryIdx)) {
645
+ const matchStart = i;
646
+ let matchLen = 0;
647
+ while (queryIdx < qLen && i < tLen && !isWordSeparator(lowerTarget.charCodeAt(i)) && lowerTarget.charCodeAt(i) === query.charCodeAt(queryIdx)) {
648
+ queryIdx++;
649
+ i++;
650
+ matchLen++;
651
+ }
652
+ matches.push([matchStart, matchStart + matchLen]);
653
+ totalWeightedLen += matchLen * 2;
654
+ } else i++;
655
+ if (queryIdx !== qLen) return null;
656
+ const maxPossible = qLen * 2;
657
+ return {
658
+ score: .3 + .4 * (totalWeightedLen / maxPossible),
659
+ matches
660
+ };
661
+ }
662
+ function scoreFuzzy(query, qLen, lowerTarget, tLen) {
663
+ let queryIdx = 0;
664
+ let targetIdx = 0;
665
+ const matches = [];
666
+ let currentMatchStart = -1;
667
+ let contiguityScore = 0;
668
+ while (queryIdx < qLen && targetIdx < tLen) if (query.charCodeAt(queryIdx) === lowerTarget.charCodeAt(targetIdx)) {
669
+ if (currentMatchStart === -1) currentMatchStart = targetIdx;
670
+ queryIdx++;
671
+ targetIdx++;
672
+ } else {
673
+ if (currentMatchStart !== -1) {
674
+ const segmentLen = targetIdx - currentMatchStart;
675
+ matches.push([currentMatchStart, targetIdx]);
676
+ contiguityScore += segmentLen * segmentLen;
677
+ currentMatchStart = -1;
678
+ }
679
+ targetIdx++;
680
+ }
681
+ if (currentMatchStart !== -1) {
682
+ const segmentLen = targetIdx - currentMatchStart;
683
+ matches.push([currentMatchStart, targetIdx]);
684
+ contiguityScore += segmentLen * segmentLen;
685
+ }
686
+ if (queryIdx !== qLen) return null;
687
+ const maxContiguity = qLen * qLen;
688
+ const contiguityRatio = contiguityScore / maxContiguity;
689
+ const positionBonus = 1 - (matches[0]?.[0] ?? 0) / tLen;
690
+ const lengthRatio = qLen / tLen;
691
+ return {
692
+ score: .1 + .3 * contiguityRatio + .15 * positionBonus + .1 * lengthRatio,
693
+ matches
694
+ };
695
+ }
696
+ //#endregion
697
+ //#region src/core/search/index.ts
698
+ const __DEV__$1 = globalThis.process ? globalThis.process.env.NODE_ENV !== "production" : false;
699
+ let leakRegistry;
700
+ if (__DEV__$1) leakRegistry = new FinalizationRegistry((label) => {
701
+ console.warn(`[modern-cmdk] ${label} was garbage collected without being disposed. Use \`using\` or call [Symbol.dispose]() to prevent leaks.`);
702
+ });
703
+ /**
704
+ * Creates a search engine with pluggable scorer and incremental filtering.
705
+ * When the query extends the previous one, only prior matches are re-scored.
706
+ * Pre-caches lowercase at index time — zero toLowerCase() calls during search.
707
+ * Uses Iterator Helpers (ES2026) for result pipelines and Set.difference for pruning.
708
+ */
709
+ function createSearchEngine(options) {
710
+ const customScorer = options?.scorer;
711
+ const useDefaultScorer = customScorer === void 0;
712
+ const indexedEntries = /* @__PURE__ */ new Map();
713
+ let previousQuery = "";
714
+ let previousResults = /* @__PURE__ */ new Set();
715
+ const sentinel = {};
716
+ if (__DEV__$1 && leakRegistry) leakRegistry.register(sentinel, "SearchEngine", sentinel);
717
+ return {
718
+ index(items) {
719
+ for (const item of items) indexedEntries.set(item.id, {
720
+ item,
721
+ lowerValue: item.value.toLowerCase(),
722
+ lowerKeywords: item.keywords?.map((k) => k.toLowerCase())
723
+ });
724
+ previousQuery = "";
725
+ previousResults = /* @__PURE__ */ new Set();
726
+ },
727
+ search(query, items) {
728
+ if (query === "") {
729
+ previousQuery = "";
730
+ previousResults = new Set(items.values().map((i) => i.id));
731
+ return items.values().map((item) => ({
732
+ id: item.id,
733
+ score: 1,
734
+ matches: []
735
+ }));
736
+ }
737
+ const lowerQuery = query.toLowerCase();
738
+ const candidateItems = query.startsWith(previousQuery) && previousQuery.length > 0 ? items.values().filter((item) => previousResults.has(item.id)) : items;
739
+ const results = [];
740
+ if (useDefaultScorer) for (const item of candidateItems) {
741
+ const entry = indexedEntries.get(item.id);
742
+ const result = entry ? scoreItemPreLowered(lowerQuery, item.id, entry.lowerValue, entry.lowerKeywords) : scoreItemPreLowered(lowerQuery, item.id, item.value.toLowerCase(), item.keywords?.map((k) => k.toLowerCase()));
743
+ if (result !== null) results.push(result);
744
+ }
745
+ else for (const item of candidateItems) {
746
+ const result = customScorer(query, item);
747
+ if (result != null) results.push(result);
748
+ }
749
+ results.sort((a, b) => b.score - a.score);
750
+ previousQuery = query;
751
+ previousResults = new Set(results.values().map((r) => r.id));
752
+ return results.values();
753
+ },
754
+ remove(ids) {
755
+ for (const id of ids) indexedEntries.delete(id);
756
+ previousResults = previousResults.difference(ids);
757
+ },
758
+ clear() {
759
+ indexedEntries.clear();
760
+ previousQuery = "";
761
+ previousResults.clear();
762
+ },
763
+ [Symbol.dispose]() {
764
+ indexedEntries.clear();
765
+ previousResults.clear();
766
+ if (__DEV__$1 && leakRegistry) leakRegistry.unregister(sentinel);
767
+ }
768
+ };
769
+ }
770
+ //#endregion
771
+ //#region src/core/utils/event-emitter.ts
772
+ /**
773
+ * Type-safe event emitter with strong references and Disposable subscriptions.
774
+ * Returns Disposable from `on()` for automatic unsubscription via `using`.
775
+ */
776
+ var TypedEmitter = class {
777
+ #listeners = /* @__PURE__ */ new Map();
778
+ /** Subscribe to an event. Returns a Disposable that removes the listener when disposed. */
779
+ on(event, listener) {
780
+ let set = this.#listeners.get(event);
781
+ if (!set) {
782
+ set = /* @__PURE__ */ new Set();
783
+ this.#listeners.set(event, set);
784
+ }
785
+ const fn = listener;
786
+ set.add(fn);
787
+ return { [Symbol.dispose]: () => {
788
+ set.delete(fn);
789
+ } };
790
+ }
791
+ /** Emit an event to all listeners. Snapshots the set to prevent live-iteration issues. */
792
+ emit(event, data) {
793
+ const set = this.#listeners.get(event);
794
+ if (!set || set.size === 0) return;
795
+ for (const fn of [...set]) fn(data);
796
+ }
797
+ /** Check if any listeners exist for an event */
798
+ has(event) {
799
+ const set = this.#listeners.get(event);
800
+ return set !== void 0 && set.size > 0;
801
+ }
802
+ /** Get count of listeners for an event */
803
+ listenerCount(event) {
804
+ return this.#listeners.get(event)?.size ?? 0;
805
+ }
806
+ /** Remove all listeners */
807
+ removeAll() {
808
+ this.#listeners.clear();
809
+ }
810
+ [Symbol.dispose]() {
811
+ this.#listeners.clear();
812
+ }
813
+ };
814
+ //#endregion
815
+ //#region src/core/utils/scheduler.ts
816
+ /** Whether we're in a browser environment with rAF available */
817
+ const hasRAF = typeof requestAnimationFrame === "function";
818
+ /** Feature detection for cutting-edge scheduling APIs */
819
+ const canYield = typeof globalThis.scheduler?.yield === "function";
820
+ const hasInputPending = typeof navigator?.scheduling?.isInputPending === "function";
821
+ /**
822
+ * Creates a batched update scheduler. Coalesces rapid state updates into a single
823
+ * execution pass using rAF (browser) or queueMicrotask (Node.js).
824
+ */
825
+ function createScheduler() {
826
+ let pending = [];
827
+ let rafId = null;
828
+ let microtaskScheduled = false;
829
+ function executeBatch() {
830
+ const batch = pending;
831
+ pending = [];
832
+ rafId = null;
833
+ microtaskScheduled = false;
834
+ if (batch.length > 10 && hasInputPending && navigator.scheduling?.isInputPending?.()) {
835
+ pending = [...batch, ...pending];
836
+ if (canYield) globalThis.scheduler?.yield?.().then(() => void executeBatch());
837
+ else rafId ??= requestAnimationFrame(() => void executeBatch());
838
+ return;
839
+ }
840
+ for (const update of batch) try {
841
+ update();
842
+ } catch (error) {
843
+ queueMicrotask(() => {
844
+ throw error;
845
+ });
846
+ }
847
+ }
848
+ function flush() {
849
+ if (rafId !== null) {
850
+ cancelAnimationFrame(rafId);
851
+ rafId = null;
852
+ }
853
+ microtaskScheduled = false;
854
+ executeBatch();
855
+ }
856
+ return {
857
+ schedule(update) {
858
+ pending.push(update);
859
+ if (hasRAF) rafId ??= requestAnimationFrame(() => void executeBatch());
860
+ else if (!microtaskScheduled) {
861
+ microtaskScheduled = true;
862
+ queueMicrotask(executeBatch);
863
+ }
864
+ },
865
+ flush,
866
+ [Symbol.dispose]() {
867
+ if (rafId !== null) {
868
+ cancelAnimationFrame(rafId);
869
+ rafId = null;
870
+ }
871
+ microtaskScheduled = false;
872
+ pending = [];
873
+ }
874
+ };
875
+ }
876
+ //#endregion
877
+ //#region src/core/machine.ts
878
+ /**
879
+ * Creates a headless command palette state machine with search, frecency,
880
+ * and keyboard shortcut support. Implements Disposable for `using` cleanup.
881
+ *
882
+ * @example
883
+ * ```ts
884
+ * using machine = createCommandMachine({ items, loop: true });
885
+ * machine.subscribe(() => console.log(machine.getState()));
886
+ * ```
887
+ */
888
+ function createCommandMachine(options = {}) {
889
+ let state = createInitialState(options);
890
+ let filteredIdSet = /* @__PURE__ */ new Set();
891
+ let filteredIdIndex = /* @__PURE__ */ new Map();
892
+ const registry = new CommandRegistry();
893
+ const searchEngine = options.search ?? createSearchEngine(typeof options.filter === "function" ? { scorer: (query, item) => {
894
+ const score = options.filter(item, query);
895
+ if (score === false) return null;
896
+ return {
897
+ id: item.id,
898
+ score,
899
+ matches: []
900
+ };
901
+ } } : void 0);
902
+ const scheduler = createScheduler();
903
+ const emitter = new TypedEmitter();
904
+ const keyboardRegistry = new KeyboardShortcutRegistry();
905
+ let frecencyEngine = null;
906
+ if (options.frecency?.enabled) {
907
+ frecencyEngine = new FrecencyEngine(options.frecency);
908
+ frecencyEngine.load();
909
+ }
910
+ if (options.items) {
911
+ registry.registerItems(options.items);
912
+ searchEngine.index(options.items);
913
+ for (const item of options.items) if (item.shortcut != null && item.onSelect != null) keyboardRegistry.register(item.shortcut, item.id, item.onSelect);
914
+ }
915
+ if (options.groups) for (const group of options.groups) registry.registerGroup(group);
916
+ refilter("");
917
+ function setState(newState) {
918
+ state = newState;
919
+ emitter.emit("stateChange", state);
920
+ }
921
+ function refilter(query) {
922
+ const items = registry.getItems();
923
+ const disableFilter = options.filter === false;
924
+ let filteredIds;
925
+ if (disableFilter || query === "") filteredIds = items.values().filter((i) => !i.disabled).map((i) => i.id).toArray();
926
+ else {
927
+ let results = searchEngine.search(query, items).toArray();
928
+ if (frecencyEngine) {
929
+ const bonuses = frecencyEngine.getAllBonuses();
930
+ results = results.toSorted((a, b) => {
931
+ const aBonus = bonuses.get(a.id) ?? 0;
932
+ const bBonus = bonuses.get(b.id) ?? 0;
933
+ return b.score + bBonus - (a.score + aBonus);
934
+ });
935
+ }
936
+ filteredIds = results.map((r) => r.id);
937
+ }
938
+ filteredIdSet = new Set(filteredIds);
939
+ filteredIdIndex = new Map(filteredIds.entries().map(([i, id]) => [id, i]));
940
+ const groupedIds = Map.groupBy(filteredIds, (id) => {
941
+ return registry.getItem(id)?.groupId ?? "__ungrouped";
942
+ });
943
+ const activeId = state.activeId && filteredIdSet.has(state.activeId) ? state.activeId : filteredIds[0] ?? null;
944
+ setState({
945
+ ...state,
946
+ search: query,
947
+ filteredIds,
948
+ groupedIds,
949
+ filteredCount: filteredIds.length,
950
+ activeId,
951
+ lastUpdated: Temporal.Now.instant()
952
+ });
953
+ }
954
+ function navigate(direction) {
955
+ const { filteredIds, activeId } = state;
956
+ if (filteredIds.length === 0) return;
957
+ if (direction === "first") {
958
+ const newActiveId = filteredIds[0] ?? null;
959
+ if (newActiveId !== activeId) {
960
+ setState({
961
+ ...state,
962
+ activeId: newActiveId,
963
+ lastUpdated: Temporal.Now.instant()
964
+ });
965
+ options.onActiveChange?.(newActiveId);
966
+ }
967
+ return;
968
+ }
969
+ if (direction === "last") {
970
+ const newActiveId = filteredIds[filteredIds.length - 1] ?? null;
971
+ if (newActiveId !== activeId) {
972
+ setState({
973
+ ...state,
974
+ activeId: newActiveId,
975
+ lastUpdated: Temporal.Now.instant()
976
+ });
977
+ options.onActiveChange?.(newActiveId);
978
+ }
979
+ return;
980
+ }
981
+ const loop = options.loop ?? true;
982
+ const currentIdx = activeId ? filteredIdIndex.get(activeId) ?? -1 : -1;
983
+ let newIdx;
984
+ if (direction === "next") if (currentIdx === -1 || currentIdx >= filteredIds.length - 1) newIdx = loop ? 0 : filteredIds.length - 1;
985
+ else newIdx = currentIdx + 1;
986
+ else if (currentIdx <= 0) newIdx = loop ? filteredIds.length - 1 : 0;
987
+ else newIdx = currentIdx - 1;
988
+ const newActiveId = filteredIds[newIdx] ?? null;
989
+ if (newActiveId !== activeId) {
990
+ setState({
991
+ ...state,
992
+ activeId: newActiveId,
993
+ lastUpdated: Temporal.Now.instant()
994
+ });
995
+ options.onActiveChange?.(newActiveId);
996
+ }
997
+ }
998
+ function handleEvent(event) {
999
+ switch (event.type) {
1000
+ case "SEARCH_CHANGE":
1001
+ refilter(event.query);
1002
+ options.onSearchChange?.(event.query);
1003
+ break;
1004
+ case "ITEM_SELECT": {
1005
+ const item = registry.getItem(event.id);
1006
+ if (item && !item.disabled) {
1007
+ item.onSelect?.();
1008
+ frecencyEngine?.recordSelection(event.id);
1009
+ emitter.emit("select", event.id);
1010
+ options.onSelect?.(event.id);
1011
+ }
1012
+ break;
1013
+ }
1014
+ case "ITEM_ACTIVATE":
1015
+ if (filteredIdSet.has(event.id)) {
1016
+ setState({
1017
+ ...state,
1018
+ activeId: event.id,
1019
+ lastUpdated: Temporal.Now.instant()
1020
+ });
1021
+ options.onActiveChange?.(event.id);
1022
+ }
1023
+ break;
1024
+ case "NAVIGATE":
1025
+ navigate(event.direction);
1026
+ break;
1027
+ case "PAGE_PUSH":
1028
+ setState({
1029
+ ...state,
1030
+ page: event.page,
1031
+ pageStack: [...state.pageStack, state.page],
1032
+ lastUpdated: Temporal.Now.instant()
1033
+ });
1034
+ break;
1035
+ case "PAGE_POP": {
1036
+ const prevPage = state.pageStack.at(-1);
1037
+ if (prevPage != null) setState({
1038
+ ...state,
1039
+ page: prevPage,
1040
+ pageStack: state.pageStack.toSpliced(-1, 1),
1041
+ lastUpdated: Temporal.Now.instant()
1042
+ });
1043
+ break;
1044
+ }
1045
+ case "OPEN":
1046
+ setState({
1047
+ ...state,
1048
+ open: true,
1049
+ lastUpdated: Temporal.Now.instant()
1050
+ });
1051
+ emitter.emit("openChange", true);
1052
+ options.onOpenChange?.(true);
1053
+ break;
1054
+ case "CLOSE":
1055
+ setState({
1056
+ ...state,
1057
+ open: false,
1058
+ lastUpdated: Temporal.Now.instant()
1059
+ });
1060
+ emitter.emit("openChange", false);
1061
+ options.onOpenChange?.(false);
1062
+ break;
1063
+ case "TOGGLE": {
1064
+ const newOpen = !state.open;
1065
+ setState({
1066
+ ...state,
1067
+ open: newOpen,
1068
+ lastUpdated: Temporal.Now.instant()
1069
+ });
1070
+ emitter.emit("openChange", newOpen);
1071
+ options.onOpenChange?.(newOpen);
1072
+ break;
1073
+ }
1074
+ case "ITEMS_LOADED":
1075
+ registry.registerItems(event.items);
1076
+ searchEngine.index(event.items);
1077
+ state = {
1078
+ ...state,
1079
+ loading: false
1080
+ };
1081
+ refilter(state.search);
1082
+ break;
1083
+ case "REGISTER_ITEM":
1084
+ registry.registerItem(event.item);
1085
+ searchEngine.index([event.item]);
1086
+ if (event.item.shortcut && event.item.onSelect) keyboardRegistry.register(event.item.shortcut, event.item.id, event.item.onSelect);
1087
+ refilter(state.search);
1088
+ break;
1089
+ case "UNREGISTER_ITEM":
1090
+ registry.unregisterItem(event.id);
1091
+ searchEngine.remove(new Set([event.id]));
1092
+ keyboardRegistry.unregisterByItem(event.id);
1093
+ refilter(state.search);
1094
+ break;
1095
+ case "REGISTER_GROUP":
1096
+ registry.registerGroup(event.group);
1097
+ refilter(state.search);
1098
+ break;
1099
+ case "UNREGISTER_GROUP":
1100
+ registry.unregisterGroup(event.id);
1101
+ refilter(state.search);
1102
+ break;
1103
+ }
1104
+ }
1105
+ return {
1106
+ getState() {
1107
+ return state;
1108
+ },
1109
+ send(event) {
1110
+ scheduler.schedule(() => handleEvent(event));
1111
+ },
1112
+ subscribe(listener) {
1113
+ const disposable = emitter.on("stateChange", listener);
1114
+ return () => disposable[Symbol.dispose]();
1115
+ },
1116
+ subscribeState(listener) {
1117
+ return emitter.on("stateChange", listener);
1118
+ },
1119
+ getRegistry() {
1120
+ return registry;
1121
+ },
1122
+ getKeyboardRegistry() {
1123
+ return keyboardRegistry;
1124
+ },
1125
+ getFilteredIdSet() {
1126
+ return filteredIdSet;
1127
+ },
1128
+ getFilteredIdIndex() {
1129
+ return filteredIdIndex;
1130
+ },
1131
+ [Symbol.dispose]() {
1132
+ scheduler[Symbol.dispose]();
1133
+ registry[Symbol.dispose]();
1134
+ searchEngine[Symbol.dispose]();
1135
+ emitter[Symbol.dispose]();
1136
+ keyboardRegistry[Symbol.dispose]();
1137
+ frecencyEngine?.[Symbol.dispose]();
1138
+ }
1139
+ };
1140
+ }
1141
+ //#endregion
64
1142
  //#region src/react/hooks/use-command.ts
1143
+ /** Core hook that binds a CommandMachine to React via useSyncExternalStore, useTransition, and useOptimistic. */
65
1144
  function useCommand(machine) {
66
1145
  const id = useId();
67
1146
  const [isPending, startTransition] = useTransition();
68
1147
  const state = useSyncExternalStore(machine.subscribe, machine.getState, machine.getState);
69
- const filteredIdSet = useMemo(() => new Set(state.filteredIds), [state.filteredIds]);
70
- const [optimisticActiveId, setOptimisticActiveId] = useOptimistic(state.activeId);
1148
+ const filteredIdSet = useMemo(() => machine.getFilteredIdSet(), [state.filteredIds, machine]);
1149
+ const [optimisticActiveId, rawSetOptimisticActiveId] = useOptimistic(state.activeId);
1150
+ const setOptimisticActiveId = useCallback((id) => {
1151
+ startTransition(() => {
1152
+ rawSetOptimisticActiveId(id);
1153
+ });
1154
+ }, [rawSetOptimisticActiveId]);
71
1155
  const updateSearch = useCallback((query) => {
72
1156
  startTransition(() => {
73
1157
  machine.send({
@@ -135,28 +1219,77 @@ function createKeydownHandler(machine, getState) {
135
1219
  };
136
1220
  }
137
1221
  //#endregion
138
- //#region src/react/dialog.tsx
139
- function CommandDialog({ children, className, overlayClassName, contentClassName, label = "Command palette", open: controlledOpen, onOpenChange, container, ...machineOptions }) {
140
- const rootId = useId();
141
- const listId = `${rootId}-list`;
142
- const inputId = `${rootId}-input`;
1222
+ //#region src/react/hooks/use-command-setup.ts
1223
+ /**
1224
+ * Shared setup for Command and Command.Dialog — creates machine once,
1225
+ * subscribes via useSyncExternalStore, memoizes context values, and
1226
+ * registers disposal on unmount.
1227
+ *
1228
+ * @param machineFactory - Called exactly once (first render) to create the machine.
1229
+ * Subsequent renders skip it via ref guard.
1230
+ * @param label - Accessible label for the command palette (included in stable context).
1231
+ */
1232
+ function useCommandSetup(machineFactory, label) {
143
1233
  const machineRef = useRef(null);
144
- if (machineRef.current === null) machineRef.current = createCommandMachine({
145
- ...machineOptions,
146
- open: controlledOpen ?? false,
147
- onOpenChange: (open) => {
148
- onOpenChange?.(open);
149
- }
150
- });
1234
+ if (machineRef.current === null) machineRef.current = machineFactory();
151
1235
  const machine = machineRef.current;
152
1236
  const { state, isPending, updateSearch, setOptimisticActiveId, filteredIdSet, id } = useCommand(machine);
1237
+ const listId = `${id}-list`;
1238
+ const inputId = `${id}-input`;
153
1239
  useEffect(() => {
154
1240
  return () => {
155
1241
  machineRef.current?.[Symbol.dispose]();
156
1242
  machineRef.current = null;
157
1243
  };
158
1244
  }, []);
159
- const handleKeyDown = useCallback(createKeydownHandler(machine, () => machine.getState()), []);
1245
+ return {
1246
+ machine,
1247
+ state,
1248
+ isPending,
1249
+ listId,
1250
+ inputId,
1251
+ handleKeyDown: useMemo(() => createKeydownHandler(machine, () => machine.getState()), [machine]),
1252
+ stableContextValue: useMemo(() => ({
1253
+ machine,
1254
+ rootId: id,
1255
+ listId,
1256
+ inputId,
1257
+ label,
1258
+ updateSearch,
1259
+ setOptimisticActiveId
1260
+ }), [
1261
+ machine,
1262
+ id,
1263
+ listId,
1264
+ inputId,
1265
+ label,
1266
+ updateSearch,
1267
+ setOptimisticActiveId
1268
+ ]),
1269
+ stateContextValue: useMemo(() => ({
1270
+ state,
1271
+ isPending,
1272
+ filteredIdSet
1273
+ }), [
1274
+ state,
1275
+ isPending,
1276
+ filteredIdSet
1277
+ ])
1278
+ };
1279
+ }
1280
+ //#endregion
1281
+ //#region src/react/dialog.tsx
1282
+ /** Dialog variant that wraps the command palette in a Radix UI Dialog with focus trap and overlay. */
1283
+ function CommandDialog({ children, className, overlayClassName, contentClassName, label = "Command palette", description = "Type a command or search...", open: controlledOpen, onOpenChange, container, ...machineOptions }) {
1284
+ const onOpenChangeRef = useRef(onOpenChange);
1285
+ onOpenChangeRef.current = onOpenChange;
1286
+ const { machine, state, handleKeyDown, stableContextValue, stateContextValue, inputId } = useCommandSetup(() => createCommandMachine({
1287
+ ...machineOptions,
1288
+ open: controlledOpen ?? false,
1289
+ onOpenChange: (open) => {
1290
+ onOpenChangeRef.current?.(open);
1291
+ }
1292
+ }), label);
160
1293
  useEffect(() => {
161
1294
  if (!state.open) return;
162
1295
  document.addEventListener("keydown", handleKeyDown);
@@ -164,39 +1297,16 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
164
1297
  document.removeEventListener("keydown", handleKeyDown);
165
1298
  };
166
1299
  }, [handleKeyDown, state.open]);
167
- const prevControlledOpen = useRef(controlledOpen);
168
1300
  useEffect(() => {
169
- if (controlledOpen !== void 0 && controlledOpen !== prevControlledOpen.current) {
170
- prevControlledOpen.current = controlledOpen;
171
- machine.send({ type: controlledOpen ? "OPEN" : "CLOSE" });
172
- }
1301
+ if (controlledOpen !== void 0) machine.send({ type: controlledOpen ? "OPEN" : "CLOSE" });
173
1302
  }, [controlledOpen, machine]);
1303
+ const handleOpenAutoFocus = useCallback((e) => {
1304
+ e.preventDefault();
1305
+ document.getElementById(inputId)?.focus();
1306
+ }, [inputId]);
174
1307
  const handleRadixOpenChange = useCallback((open) => {
175
1308
  machine.send({ type: open ? "OPEN" : "CLOSE" });
176
1309
  }, [machine]);
177
- const contextValue = useMemo(() => ({
178
- machine,
179
- state,
180
- isPending,
181
- updateSearch,
182
- setOptimisticActiveId,
183
- filteredIdSet,
184
- rootId: id,
185
- listId,
186
- inputId,
187
- label
188
- }), [
189
- machine,
190
- state,
191
- isPending,
192
- updateSearch,
193
- setOptimisticActiveId,
194
- filteredIdSet,
195
- id,
196
- listId,
197
- inputId,
198
- label
199
- ]);
200
1310
  return /* @__PURE__ */ jsx(Dialog.Root, {
201
1311
  open: state.open,
202
1312
  onOpenChange: handleRadixOpenChange,
@@ -211,10 +1321,7 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
211
1321
  "data-state": state.open ? "open" : "closed",
212
1322
  className,
213
1323
  "aria-label": label,
214
- onOpenAutoFocus: (e) => {
215
- e.preventDefault();
216
- document.getElementById(inputId)?.focus();
217
- },
1324
+ onOpenAutoFocus: handleOpenAutoFocus,
218
1325
  children: [
219
1326
  /* @__PURE__ */ jsx(Dialog.Title, {
220
1327
  className: "sr-only",
@@ -222,18 +1329,21 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
222
1329
  }),
223
1330
  /* @__PURE__ */ jsx(Dialog.Description, {
224
1331
  className: "sr-only",
225
- children: "Type a command or search..."
1332
+ children: description
226
1333
  }),
227
- /* @__PURE__ */ jsx(CommandContext, {
228
- value: contextValue,
229
- children: /* @__PURE__ */ jsx("div", {
230
- "data-command-dialog-content": "",
231
- className: contentClassName,
1334
+ /* @__PURE__ */ jsx(CommandStableContext, {
1335
+ value: stableContextValue,
1336
+ children: /* @__PURE__ */ jsx(CommandStateContext, {
1337
+ value: stateContextValue,
232
1338
  children: /* @__PURE__ */ jsx("div", {
233
- "data-command-root": "",
234
- role: "search",
235
- "aria-label": label,
236
- children
1339
+ "data-command-dialog-content": "",
1340
+ className: contentClassName,
1341
+ children: /* @__PURE__ */ jsx("div", {
1342
+ "data-command-root": "",
1343
+ role: "search",
1344
+ "aria-label": label,
1345
+ children
1346
+ })
237
1347
  })
238
1348
  })
239
1349
  })
@@ -244,11 +1354,12 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
244
1354
  }
245
1355
  //#endregion
246
1356
  //#region src/react/empty.tsx
1357
+ /** Rendered when no items match the current search query. */
247
1358
  function CommandEmpty({ ref, children, ...props }) {
248
- const ctx = use(CommandContext);
249
- if (!ctx) throw new Error("Command.Empty must be used within a <Command> component");
250
- if (ctx.state.filteredCount > 0 || ctx.state.loading) return null;
251
- return /* @__PURE__ */ jsx("div", {
1359
+ const stateCtx = useStateContext("Command.Empty");
1360
+ const statusContainerRef = use(CommandListStatusContext);
1361
+ if (stateCtx.state.filteredCount > 0 || stateCtx.state.loading) return null;
1362
+ const content = /* @__PURE__ */ jsx("div", {
252
1363
  ref,
253
1364
  "data-command-empty": "",
254
1365
  role: "status",
@@ -256,16 +1367,22 @@ function CommandEmpty({ ref, children, ...props }) {
256
1367
  ...props,
257
1368
  children
258
1369
  });
1370
+ if (statusContainerRef?.current) return createPortal(content, statusContainerRef.current);
1371
+ return content;
259
1372
  }
260
1373
  //#endregion
261
1374
  //#region src/react/hooks/use-register.ts
262
1375
  /** Register a command item with the machine — auto-deregisters on unmount */
263
1376
  function useRegisterItem(value, options) {
264
- const ctx = use(CommandContext);
265
- if (!ctx) throw new Error("useRegisterItem must be used within a <Command> component");
1377
+ const stable = useStableContext("useRegisterItem");
266
1378
  const generatedId = useId();
267
1379
  const id = itemId(options?.forceId ?? generatedId);
268
- useInsertionEffect(() => {
1380
+ const onSelectRef = useRef(options?.onSelect);
1381
+ onSelectRef.current = options?.onSelect;
1382
+ const dataRef = useRef(options?.data);
1383
+ dataRef.current = options?.data;
1384
+ const keywordsDep = options?.keywords?.join("\0");
1385
+ useLayoutEffect(() => {
269
1386
  const item = {
270
1387
  id,
271
1388
  value,
@@ -273,56 +1390,53 @@ function useRegisterItem(value, options) {
273
1390
  groupId: options?.groupId ? groupId(options.groupId) : void 0,
274
1391
  shortcut: options?.shortcut,
275
1392
  disabled: options?.disabled,
276
- onSelect: options?.onSelect,
277
- data: options?.data
1393
+ onSelect: () => onSelectRef.current?.(),
1394
+ data: dataRef.current
278
1395
  };
279
- ctx.machine.send({
1396
+ stable.machine.send({
280
1397
  type: "REGISTER_ITEM",
281
1398
  item
282
1399
  });
283
1400
  return () => {
284
- ctx.machine.send({
1401
+ stable.machine.send({
285
1402
  type: "UNREGISTER_ITEM",
286
1403
  id
287
1404
  });
288
1405
  };
289
1406
  }, [
290
- ctx.machine,
1407
+ stable.machine,
291
1408
  id,
292
1409
  value,
293
1410
  options?.groupId,
294
- options?.data,
295
1411
  options?.disabled,
296
- options?.keywords,
297
- options?.onSelect,
1412
+ keywordsDep,
298
1413
  options?.shortcut
299
1414
  ]);
300
1415
  return id;
301
1416
  }
302
1417
  /** Register a command group with the machine — auto-deregisters on unmount */
303
1418
  function useRegisterGroup(heading, options) {
304
- const ctx = use(CommandContext);
305
- if (!ctx) throw new Error("useRegisterGroup must be used within a <Command> component");
1419
+ const stable = useStableContext("useRegisterGroup");
306
1420
  const generatedId = useId();
307
1421
  const id = groupId(options?.forceId ?? generatedId);
308
- useInsertionEffect(() => {
1422
+ useLayoutEffect(() => {
309
1423
  const group = {
310
1424
  id,
311
1425
  heading,
312
1426
  priority: options?.priority
313
1427
  };
314
- ctx.machine.send({
1428
+ stable.machine.send({
315
1429
  type: "REGISTER_GROUP",
316
1430
  group
317
1431
  });
318
1432
  return () => {
319
- ctx.machine.send({
1433
+ stable.machine.send({
320
1434
  type: "UNREGISTER_GROUP",
321
1435
  id
322
1436
  });
323
1437
  };
324
1438
  }, [
325
- ctx.machine,
1439
+ stable.machine,
326
1440
  id,
327
1441
  heading,
328
1442
  options?.priority
@@ -331,21 +1445,29 @@ function useRegisterGroup(heading, options) {
331
1445
  }
332
1446
  //#endregion
333
1447
  //#region src/react/group.tsx
1448
+ /** Module-level constant — avoids allocating a new object on every render of hidden groups */
1449
+ const HIDDEN_STYLE = { display: "none" };
1450
+ /** Visual grouping container with an optional heading that hides when no items match. */
334
1451
  function CommandGroup({ ref, children, heading, priority, forceId, ...props }) {
335
- const ctx = use(CommandContext);
336
- if (!ctx) throw new Error("Command.Group must be used within a <Command> component");
1452
+ const stateCtx = useStateContext("Command.Group");
337
1453
  const headingId = useId();
338
1454
  const groupId = useRegisterGroup(heading, {
339
1455
  ...priority !== void 0 && { priority },
340
1456
  ...forceId !== void 0 && { forceId }
341
1457
  });
342
- const groupItems = ctx.state.groupedIds.get(groupId);
1458
+ const groupItems = stateCtx.state.groupedIds.get(groupId);
343
1459
  const hasVisibleItems = groupItems !== void 0 && groupItems.length > 0;
344
1460
  const groupContext = useMemo(() => ({
345
1461
  groupId,
346
1462
  headingId
347
1463
  }), [groupId, headingId]);
348
- if (!hasVisibleItems) return null;
1464
+ if (!hasVisibleItems) return /* @__PURE__ */ jsx(CommandGroupContext, {
1465
+ value: groupContext,
1466
+ children: /* @__PURE__ */ jsx("div", {
1467
+ style: HIDDEN_STYLE,
1468
+ children
1469
+ })
1470
+ });
349
1471
  return /* @__PURE__ */ jsx(CommandGroupContext, {
350
1472
  value: groupContext,
351
1473
  children: /* @__PURE__ */ jsxs("div", {
@@ -368,13 +1490,14 @@ function CommandGroup({ ref, children, heading, priority, forceId, ...props }) {
368
1490
  }
369
1491
  //#endregion
370
1492
  //#region src/react/highlight.tsx
1493
+ /** Renders search match highlighting by splitting text at scored match ranges. */
371
1494
  function CommandHighlight({ text, ranges, highlightClassName }) {
372
1495
  return useMemo(() => {
373
1496
  if (ranges.length === 0) return text;
374
- const sorted = Iterator.from(ranges).toArray().sort((a, b) => a[0] - b[0]);
1497
+ const sorted = ranges.toSorted((a, b) => a[0] - b[0]);
375
1498
  const segments = [];
376
1499
  let lastEnd = 0;
377
- Iterator.from(sorted).forEach(([start, end], i) => {
1500
+ sorted.forEach(([start, end], i) => {
378
1501
  if (start > lastEnd) segments.push(text.slice(lastEnd, start));
379
1502
  segments.push(/* @__PURE__ */ jsx("mark", {
380
1503
  "data-command-highlight": "",
@@ -393,71 +1516,75 @@ function CommandHighlight({ text, ranges, highlightClassName }) {
393
1516
  }
394
1517
  //#endregion
395
1518
  //#region src/react/input.tsx
1519
+ /** Combobox input that drives the command palette search state. */
396
1520
  function CommandInput({ ref, placeholder = "Search...", onValueChange, ...props }) {
397
- const ctx = use(CommandContext);
398
- if (!ctx) throw new Error("Command.Input must be used within a <Command> component");
1521
+ const stable = useStableContext("Command.Input");
1522
+ const stateCtx = useStateContext("Command.Input");
399
1523
  const handleChange = useCallback((event) => {
400
1524
  const { value } = event.target;
401
- ctx.updateSearch(value);
1525
+ stable.updateSearch(value);
402
1526
  onValueChange?.(value);
403
- }, [ctx.updateSearch, onValueChange]);
1527
+ }, [stable.updateSearch, onValueChange]);
404
1528
  return /* @__PURE__ */ jsx("input", {
405
1529
  ref,
406
1530
  "data-command-input": "",
407
1531
  type: "text",
408
1532
  role: "combobox",
409
- "aria-expanded": ctx.state.filteredCount > 0,
410
- "aria-controls": ctx.listId,
411
- "aria-activedescendant": ctx.state.activeId ?? void 0,
1533
+ "aria-expanded": true,
1534
+ "aria-controls": stable.listId,
1535
+ "aria-activedescendant": stateCtx.state.activeId ?? void 0,
412
1536
  "aria-autocomplete": "list",
413
- "aria-label": ctx.label,
414
- id: ctx.inputId,
1537
+ "aria-label": stable.label,
1538
+ id: stable.inputId,
415
1539
  autoComplete: "off",
416
1540
  autoCorrect: "off",
417
1541
  spellCheck: false,
418
1542
  placeholder,
419
- value: ctx.state.search,
1543
+ value: stateCtx.state.search,
420
1544
  onChange: handleChange,
421
1545
  ...props
422
1546
  });
423
1547
  }
424
1548
  //#endregion
425
1549
  //#region src/react/item.tsx
1550
+ /** Individual selectable item in the command list with keyboard and pointer activation. */
426
1551
  function CommandItem({ ref, children, value, keywords, onSelect, disabled = false, shortcut, forceId, ...props }) {
427
- const ctx = use(CommandContext);
428
- if (!ctx) throw new Error("Command.Item must be used within a <Command> component");
1552
+ const stable = useStableContext("Command.Item");
1553
+ const stateCtx = useStateContext("Command.Item");
1554
+ const groupCtx = use(CommandGroupContext);
429
1555
  const id = useRegisterItem(value, {
430
1556
  ...keywords !== void 0 && { keywords },
431
1557
  ...shortcut !== void 0 && { shortcut },
432
1558
  disabled,
433
1559
  ...onSelect !== void 0 && { onSelect },
434
- ...forceId !== void 0 && { forceId }
1560
+ ...forceId !== void 0 && { forceId },
1561
+ ...groupCtx !== null && { groupId: groupCtx.groupId }
435
1562
  });
436
- const isActive = ctx.state.activeId === id;
437
- const isFiltered = ctx.filteredIdSet.has(id);
1563
+ const isActive = stateCtx.state.activeId === id;
1564
+ const isFiltered = stateCtx.filteredIdSet.has(id);
438
1565
  const handleSelect = useCallback(() => {
439
- if (!disabled) ctx.machine.send({
1566
+ if (!disabled) stable.machine.send({
440
1567
  type: "ITEM_SELECT",
441
1568
  id
442
1569
  });
443
1570
  }, [
444
- ctx.machine,
1571
+ stable.machine,
445
1572
  id,
446
1573
  disabled
447
1574
  ]);
448
- const activeIdRef = useRef(ctx.state.activeId);
449
- activeIdRef.current = ctx.state.activeId;
1575
+ const activeIdRef = useRef(stateCtx.state.activeId);
1576
+ activeIdRef.current = stateCtx.state.activeId;
450
1577
  const handlePointerMove = useCallback(() => {
451
1578
  if (!disabled && activeIdRef.current !== id) {
452
- ctx.setOptimisticActiveId(id);
453
- ctx.machine.send({
1579
+ stable.setOptimisticActiveId(id);
1580
+ stable.machine.send({
454
1581
  type: "ITEM_ACTIVATE",
455
1582
  id
456
1583
  });
457
1584
  }
458
1585
  }, [
459
- ctx.machine,
460
- ctx.setOptimisticActiveId,
1586
+ stable.machine,
1587
+ stable.setOptimisticActiveId,
461
1588
  id,
462
1589
  disabled
463
1590
  ]);
@@ -481,6 +1608,7 @@ function CommandItem({ ref, children, value, keywords, onSelect, disabled = fals
481
1608
  }
482
1609
  //#endregion
483
1610
  //#region src/react/hooks/use-virtualizer.ts
1611
+ /** Lightweight virtualizer for large item lists using ResizeObserver and scroll tracking. */
484
1612
  function useVirtualizer(options) {
485
1613
  const { count, estimateSize, overscan = 8, scrollElement, enabled = true } = options;
486
1614
  const [scrollOffset, setScrollOffset] = useState(0);
@@ -494,26 +1622,20 @@ function useVirtualizer(options) {
494
1622
  if (lastEntry) setContainerHeight(lastEntry.contentRect.height);
495
1623
  });
496
1624
  observer.observe(scrollElement);
497
- return () => {
498
- observer.disconnect();
499
- };
500
- }, [scrollElement, enabled]);
501
- useEffect(() => {
502
- if (!scrollElement || !enabled) return;
503
1625
  const handleScroll = () => {
504
1626
  setScrollOffset(scrollElement.scrollTop);
505
1627
  };
506
1628
  scrollElement.addEventListener("scroll", handleScroll, { passive: true });
507
1629
  return () => {
1630
+ observer.disconnect();
508
1631
  scrollElement.removeEventListener("scroll", handleScroll);
509
1632
  };
510
1633
  }, [scrollElement, enabled]);
511
1634
  const getItemSize = useCallback((index) => measuredSizes.current.get(index) ?? estimateSize, [estimateSize]);
512
1635
  const totalSize = useMemo(() => {
513
1636
  if (!enabled) return 0;
514
- const sizes = Iterator.from({ [Symbol.iterator]: function* () {
515
- for (let i = 0; i < count; i++) yield getItemSize(i);
516
- } }).toArray();
1637
+ const sizes = [];
1638
+ for (let i = 0; i < count; i++) sizes.push(getItemSize(i));
517
1639
  return Math.sumPrecise(sizes);
518
1640
  }, [
519
1641
  enabled,
@@ -531,9 +1653,8 @@ function useVirtualizer(options) {
531
1653
  startIdx++;
532
1654
  }
533
1655
  startIdx = Math.max(0, startIdx - overscan);
534
- const offsetSizes = Iterator.from({ [Symbol.iterator]: function* () {
535
- for (let i = 0; i < startIdx; i++) yield getItemSize(i);
536
- } }).toArray();
1656
+ const offsetSizes = [];
1657
+ for (let i = 0; i < startIdx; i++) offsetSizes.push(getItemSize(i));
537
1658
  offset = Math.sumPrecise(offsetSizes);
538
1659
  const items = [];
539
1660
  let currentOffset = offset;
@@ -562,10 +1683,9 @@ function useVirtualizer(options) {
562
1683
  totalSize,
563
1684
  scrollToIndex: useCallback((index) => {
564
1685
  if (!scrollElement || !enabled) return;
565
- const clampedIndex = Math.min(index, count);
566
- const sizes = Iterator.from({ [Symbol.iterator]: function* () {
567
- for (let i = 0; i < clampedIndex; i++) yield getItemSize(i);
568
- } }).toArray();
1686
+ const clampedIndex = Math.min(index, count - 1);
1687
+ const sizes = [];
1688
+ for (let i = 0; i < clampedIndex; i++) sizes.push(getItemSize(i));
569
1689
  const targetOffset = Math.sumPrecise(sizes);
570
1690
  scrollElement.scrollTo({
571
1691
  top: targetOffset,
@@ -595,13 +1715,21 @@ function useVirtualizer(options) {
595
1715
  }
596
1716
  //#endregion
597
1717
  //#region src/react/list.tsx
598
- function CommandList({ ref, children, virtualize, estimateSize = 44, overscan = 8, style, ...props }) {
599
- const ctx = use(CommandContext);
600
- if (!ctx) throw new Error("Command.List must be used within a <Command> component");
1718
+ /** Base style for virtual items — hoisted to avoid allocation per item per render */
1719
+ const VIRTUAL_ITEM_BASE_STYLE = {
1720
+ position: "absolute",
1721
+ top: 0,
1722
+ left: 0,
1723
+ width: "100%"
1724
+ };
1725
+ /** Scrollable list container with auto-virtualization and aria-live announcements. */
1726
+ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan = 8, announceResults, style, ...props }) {
1727
+ const stable = useStableContext("Command.List");
1728
+ const stateCtx = useStateContext("Command.List");
601
1729
  const innerRef = useRef(null);
602
1730
  const scrollRef = useRef(null);
603
1731
  const [height, setHeight] = useState(0);
604
- const shouldVirtualize = virtualize ?? ctx.state.filteredCount > 100;
1732
+ const shouldVirtualize = virtualize ?? stateCtx.state.filteredCount > 100;
605
1733
  useEffect(() => {
606
1734
  const element = innerRef.current;
607
1735
  if (!element) return;
@@ -615,90 +1743,97 @@ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan =
615
1743
  };
616
1744
  }, []);
617
1745
  const virtualizer = useVirtualizer({
618
- count: ctx.state.filteredCount,
1746
+ count: stateCtx.state.filteredCount,
619
1747
  estimateSize,
620
1748
  overscan,
621
1749
  scrollElement: shouldVirtualize ? scrollRef.current : null,
622
1750
  enabled: shouldVirtualize
623
1751
  });
624
- const prevActiveId = useRef(ctx.state.activeId);
1752
+ const prevActiveId = useRef(stateCtx.state.activeId);
625
1753
  useEffect(() => {
626
- if (ctx.state.activeId && ctx.state.activeId !== prevActiveId.current) {
627
- prevActiveId.current = ctx.state.activeId;
1754
+ if (stateCtx.state.activeId && stateCtx.state.activeId !== prevActiveId.current) {
1755
+ prevActiveId.current = stateCtx.state.activeId;
628
1756
  if (shouldVirtualize) {
629
- const idx = ctx.state.filteredIds.indexOf(ctx.state.activeId);
1757
+ const idx = stable.machine.getFilteredIdIndex().get(stateCtx.state.activeId) ?? -1;
630
1758
  if (idx >= 0) virtualizer.scrollToIndex(idx);
631
- } else (scrollRef.current?.querySelector(`[id="${CSS.escape(ctx.state.activeId)}"]`))?.scrollIntoView({
1759
+ } else (scrollRef.current?.querySelector(`[id="${CSS.escape(stateCtx.state.activeId)}"]`))?.scrollIntoView({
632
1760
  block: "nearest",
633
1761
  behavior: "smooth"
634
1762
  });
635
1763
  }
636
1764
  }, [
637
- ctx.state.activeId,
638
- ctx.state.filteredIds,
1765
+ stateCtx.state.activeId,
639
1766
  shouldVirtualize,
640
- virtualizer
1767
+ virtualizer,
1768
+ stable.machine.getFilteredIdIndex
641
1769
  ]);
642
- const mergedStyle = {
1770
+ const mergedStyle = useMemo(() => ({
643
1771
  ...style,
644
1772
  "--command-list-height": `${height.toFixed(1)}px`
645
- };
646
- return /* @__PURE__ */ jsxs("div", {
647
- ref: useCallback((el) => {
648
- scrollRef.current = el;
649
- if (typeof ref === "function") ref(el);
650
- else if (ref) ref.current = el;
651
- }, [ref]),
652
- "data-command-list": "",
653
- role: "listbox",
654
- "aria-label": ctx.label,
655
- "aria-busy": ctx.state.loading,
656
- id: ctx.listId,
657
- style: mergedStyle,
658
- ...props,
659
- children: [shouldVirtualize ? /* @__PURE__ */ jsxs("div", {
660
- "data-command-list-virtual": "",
661
- style: {
662
- height: `${virtualizer.totalSize}px`,
663
- position: "relative"
664
- },
665
- children: [virtualizer.virtualItems.map((vItem) => /* @__PURE__ */ jsx("div", {
666
- "data-command-virtual-item": "",
667
- "data-index": vItem.index,
668
- ref: virtualizer.measureElement,
669
- style: {
670
- position: "absolute",
671
- top: 0,
672
- left: 0,
673
- width: "100%",
674
- translate: `0 ${vItem.start}px`
675
- }
676
- }, vItem.key)), children]
677
- }) : /* @__PURE__ */ jsx("div", {
678
- ref: innerRef,
679
- "data-command-list-inner": "",
680
- children
681
- }), /* @__PURE__ */ jsxs("div", {
682
- "aria-live": "polite",
683
- "aria-atomic": true,
684
- role: "status",
685
- "data-command-aria-live": "",
686
- className: "sr-only",
687
- children: [
688
- ctx.state.filteredCount,
689
- " result",
690
- ctx.state.filteredCount !== 1 ? "s" : "",
691
- " available."
692
- ]
693
- })]
1773
+ }), [style, height]);
1774
+ const externalRefRef = useRef(ref);
1775
+ externalRefRef.current = ref;
1776
+ const setScrollRef = useCallback((el) => {
1777
+ scrollRef.current = el;
1778
+ const externalRef = externalRefRef.current;
1779
+ if (typeof externalRef === "function") externalRef(el);
1780
+ else if (externalRef) externalRef.current = el;
1781
+ }, []);
1782
+ const statusContainerRef = useRef(null);
1783
+ return /* @__PURE__ */ jsxs(CommandListStatusContext, {
1784
+ value: statusContainerRef,
1785
+ children: [
1786
+ /* @__PURE__ */ jsx("div", {
1787
+ ref: setScrollRef,
1788
+ "data-command-list": "",
1789
+ role: "listbox",
1790
+ "aria-label": stable.label,
1791
+ "aria-busy": stateCtx.state.loading,
1792
+ id: stable.listId,
1793
+ style: mergedStyle,
1794
+ ...props,
1795
+ children: shouldVirtualize ? /* @__PURE__ */ jsxs("div", {
1796
+ "data-command-list-virtual": "",
1797
+ style: {
1798
+ height: `${virtualizer.totalSize}px`,
1799
+ position: "relative"
1800
+ },
1801
+ children: [virtualizer.virtualItems.map((vItem) => /* @__PURE__ */ jsx("div", {
1802
+ "data-command-virtual-item": "",
1803
+ "data-index": vItem.index,
1804
+ ref: virtualizer.measureElement,
1805
+ style: {
1806
+ ...VIRTUAL_ITEM_BASE_STYLE,
1807
+ translate: `0 ${vItem.start}px`
1808
+ }
1809
+ }, vItem.key)), children]
1810
+ }) : /* @__PURE__ */ jsx("div", {
1811
+ ref: innerRef,
1812
+ "data-command-list-inner": "",
1813
+ children
1814
+ })
1815
+ }),
1816
+ /* @__PURE__ */ jsx("div", {
1817
+ ref: statusContainerRef,
1818
+ "data-command-list-status": ""
1819
+ }),
1820
+ /* @__PURE__ */ jsx("div", {
1821
+ "aria-live": "polite",
1822
+ "aria-atomic": true,
1823
+ role: "status",
1824
+ "data-command-aria-live": "",
1825
+ className: "sr-only",
1826
+ children: announceResults ? announceResults(stateCtx.state.filteredCount) : `${stateCtx.state.filteredCount} result${stateCtx.state.filteredCount !== 1 ? "s" : ""} available.`
1827
+ })
1828
+ ]
694
1829
  });
695
1830
  }
696
1831
  //#endregion
697
1832
  //#region src/react/loading.tsx
1833
+ /** Shown during async operations, with configurable loading state override. */
698
1834
  function CommandLoading({ ref, children, loading: loadingOverride, ...props }) {
699
- const ctx = use(CommandContext);
700
- if (!ctx) throw new Error("Command.Loading must be used within a <Command> component");
701
- if (!(loadingOverride ?? ctx.state.loading ?? ctx.isPending)) return null;
1835
+ const stateCtx = useStateContext("Command.Loading");
1836
+ if (!(loadingOverride ?? (stateCtx.state.loading || stateCtx.isPending))) return null;
702
1837
  return /* @__PURE__ */ jsx("div", {
703
1838
  ref,
704
1839
  "data-command-loading": "",
@@ -711,10 +1846,9 @@ function CommandLoading({ ref, children, loading: loadingOverride, ...props }) {
711
1846
  }
712
1847
  //#endregion
713
1848
  //#region src/react/page.tsx
1849
+ /** Page navigation container that only renders when its ID matches the active page. */
714
1850
  function CommandPage({ id, children }) {
715
- const ctx = use(CommandContext);
716
- if (!ctx) throw new Error("Command.Page must be used within a <Command> component");
717
- if (ctx.state.page !== id) return null;
1851
+ if (useStateContext("Command.Page").state.page !== id) return null;
718
1852
  return /* @__PURE__ */ jsx("div", {
719
1853
  "data-command-page": "",
720
1854
  "data-command-page-id": id,
@@ -723,81 +1857,57 @@ function CommandPage({ id, children }) {
723
1857
  }
724
1858
  //#endregion
725
1859
  //#region src/react/separator.tsx
1860
+ /** Visual divider between items or groups, rendered as a presentational element. */
726
1861
  function CommandSeparator({ ref, ...props }) {
727
1862
  return /* @__PURE__ */ jsx("div", {
728
1863
  ref,
729
1864
  "data-command-separator": "",
730
- role: "separator",
1865
+ role: "none",
731
1866
  ...props
732
1867
  });
733
1868
  }
734
1869
  //#endregion
735
1870
  //#region src/react/shortcut.tsx
1871
+ /** Keyboard shortcut display with platform-aware formatting (e.g., Cmd vs Ctrl). */
736
1872
  function CommandShortcut({ ref, shortcut, children, ...props }) {
737
- const displayText = useMemo(() => shortcut ? formatShortcut(parseShortcut(shortcut)) : children, [shortcut, children]);
1873
+ const formattedShortcut = useMemo(() => shortcut ? formatShortcut(parseShortcut(shortcut)) : null, [shortcut]);
738
1874
  return /* @__PURE__ */ jsx("kbd", {
739
1875
  ref,
740
1876
  "data-command-shortcut": "",
741
1877
  ...props,
742
- children: displayText
1878
+ children: formattedShortcut ?? children
743
1879
  });
744
1880
  }
745
1881
  //#endregion
746
1882
  //#region src/react/command.tsx
1883
+ /** Root compound component that provides the command state machine context to all children. */
747
1884
  function CommandRoot({ children, className, label = "Command palette", ...machineOptions }) {
748
- const rootId = useId();
749
- const listId = `${rootId}-list`;
750
- const inputId = `${rootId}-input`;
751
1885
  const rootRef = useRef(null);
752
- const machineRef = useRef(null);
753
- if (machineRef.current === null) machineRef.current = createCommandMachine(machineOptions);
754
- const machine = machineRef.current;
755
- const { state, isPending, updateSearch, setOptimisticActiveId, filteredIdSet, id } = useCommand(machine);
1886
+ const { state, handleKeyDown, stableContextValue, stateContextValue } = useCommandSetup(() => createCommandMachine(machineOptions), label);
756
1887
  useEffect(() => {
757
- return () => {
758
- machineRef.current?.[Symbol.dispose]();
759
- machineRef.current = null;
1888
+ const root = rootRef.current;
1889
+ if (!root) return;
1890
+ const scopedHandler = (e) => {
1891
+ if (root.contains(e.target)) handleKeyDown(e);
760
1892
  };
761
- }, []);
762
- const handleKeyDown = useCallback(createKeydownHandler(machine, () => machine.getState()), []);
763
- useEffect(() => {
764
- document.addEventListener("keydown", handleKeyDown);
1893
+ document.addEventListener("keydown", scopedHandler);
765
1894
  return () => {
766
- document.removeEventListener("keydown", handleKeyDown);
1895
+ document.removeEventListener("keydown", scopedHandler);
767
1896
  };
768
1897
  }, [handleKeyDown]);
769
- return /* @__PURE__ */ jsx(CommandContext, {
770
- value: useMemo(() => ({
771
- machine,
772
- state,
773
- isPending,
774
- updateSearch,
775
- setOptimisticActiveId,
776
- filteredIdSet,
777
- rootId: id,
778
- listId,
779
- inputId,
780
- label
781
- }), [
782
- machine,
783
- state,
784
- isPending,
785
- updateSearch,
786
- setOptimisticActiveId,
787
- filteredIdSet,
788
- id,
789
- listId,
790
- inputId,
791
- label
792
- ]),
793
- children: /* @__PURE__ */ jsx("div", {
794
- ref: rootRef,
795
- "data-command-root": "",
796
- "data-command-state": state.open ? "open" : "closed",
797
- className,
798
- role: "search",
799
- "aria-label": label,
800
- children
1898
+ return /* @__PURE__ */ jsx(CommandStableContext, {
1899
+ value: stableContextValue,
1900
+ children: /* @__PURE__ */ jsx(CommandStateContext, {
1901
+ value: stateContextValue,
1902
+ children: /* @__PURE__ */ jsx("div", {
1903
+ ref: rootRef,
1904
+ "data-command-root": "",
1905
+ "data-command-state": state.open ? "open" : "closed",
1906
+ className,
1907
+ role: "search",
1908
+ "aria-label": label,
1909
+ children
1910
+ })
801
1911
  })
802
1912
  });
803
1913
  }
@@ -822,6 +1932,7 @@ const Command = Object.assign(CommandRoot, {
822
1932
  //#region src/react/error-boundary.tsx
823
1933
  /** Initial state — no error captured */
824
1934
  const INITIAL_STATE = { error: null };
1935
+ /** Error boundary for graceful failure recovery in command subtrees. */
825
1936
  var CommandErrorBoundary = class extends Component {
826
1937
  state = INITIAL_STATE;
827
1938
  static getDerivedStateFromError(error) {
@@ -843,10 +1954,11 @@ var CommandErrorBoundary = class extends Component {
843
1954
  //#endregion
844
1955
  //#region src/react/hooks/use-command-state.ts
845
1956
  function useCommandState(selector) {
846
- const ctx = use(CommandContext);
847
- if (!ctx) throw new Error("useCommandState must be used within a <Command> component");
848
- const getSnapshot = useCallback(() => selector ? selector(ctx.machine.getState()) : ctx.machine.getState(), [ctx.machine, selector]);
849
- return useSyncExternalStore(ctx.machine.subscribe, getSnapshot, getSnapshot);
1957
+ const stable = useStableContext("useCommandState");
1958
+ const selectorRef = useRef(selector);
1959
+ selectorRef.current = selector;
1960
+ const getSnapshot = useCallback(() => selectorRef.current ? selectorRef.current(stable.machine.getState()) : stable.machine.getState(), [stable.machine]);
1961
+ return useSyncExternalStore(stable.machine.subscribe, getSnapshot, getSnapshot);
850
1962
  }
851
1963
  //#endregion
852
1964
  //#region src/react/hooks/use-devtools.ts
@@ -865,15 +1977,16 @@ const DEVTOOLS_KEY = "__CRIMSON_COMMAND_DEVTOOLS__";
865
1977
  * ```
866
1978
  */
867
1979
  function useCommandDevtools(label = "default") {
868
- if (!__DEV__) return;
869
- const ctx = use(CommandContext);
1980
+ const stable = use(CommandStableContext);
1981
+ const stateCtx = use(CommandStateContext);
870
1982
  const stateRef = useRef(null);
871
1983
  useEffect(() => {
872
- if (!ctx) return;
1984
+ if (!__DEV__) return;
1985
+ if (!stable || !stateCtx) return;
873
1986
  const update = () => {
874
- const s = ctx.state;
1987
+ const s = stateCtx.state;
875
1988
  const devState = {
876
- registeredItems: s.filteredCount,
1989
+ registeredItems: stable.machine.getRegistry().size,
877
1990
  filteredItems: s.filteredIds.length,
878
1991
  searchQuery: s.search,
879
1992
  activeId: s.activeId,
@@ -891,15 +2004,17 @@ function useCommandDevtools(label = "default") {
891
2004
  state: devState
892
2005
  } }));
893
2006
  };
894
- const unsub = ctx.machine.subscribe(update);
2007
+ const unsub = stable.machine.subscribe(update);
895
2008
  update();
896
2009
  return () => {
897
2010
  unsub();
898
2011
  globalThis[DEVTOOLS_KEY]?.delete(label);
899
2012
  };
900
- }, [ctx, label]);
2013
+ }, [
2014
+ stable,
2015
+ stateCtx,
2016
+ label
2017
+ ]);
901
2018
  }
902
2019
  //#endregion
903
- export { Command, CommandActivity, CommandAsyncItems, CommandBadge, CommandDialog, CommandEmpty, CommandErrorBoundary, CommandGroup, CommandHighlight, CommandInput, CommandItem, CommandList, CommandLoading, CommandPage, CommandSeparator, CommandShortcut, createKeydownHandler, useCommand, useCommandDevtools, useCommandState, useRegisterGroup, useRegisterItem, useVirtualizer };
904
-
905
- //# sourceMappingURL=index.mjs.map
2020
+ export { Command, CommandActivity, CommandAsyncItems, CommandBadge, CommandDialog, CommandEmpty, CommandErrorBoundary, CommandGroup, CommandHighlight, CommandInput, CommandItem, CommandList, CommandLoading, CommandPage, CommandSeparator, CommandShortcut, CommandStableContext, CommandStateContext, createKeydownHandler, useCommand, useCommandDevtools, useCommandState, useRegisterGroup, useRegisterItem, useVirtualizer };