import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from 'react'; import type { Command } from './types.js'; interface RegistryState { commands: Map; register: (commands: Command[]) => () => void; unregister: (ids: string[]) => void; clear: () => void; } const RegistryContext = createContext(null); export interface CommandRegistryProviderProps { /** Optional seed of commands always available (app-level chrome). */ initial?: Command[]; children: ReactNode; } /** * Provides a shared command registry to descendants. Wrap your app once * (typically next to ``) and any nested component can * call `useRegisterCommands(...)` to contribute commands for as long as * it stays mounted. */ export function CommandRegistryProvider({ initial = [], children, }: CommandRegistryProviderProps) { const [commands, setCommands] = useState>(() => { const m = new Map(); for (const c of initial) m.set(c.id, c); return m; }); const register = useCallback(next => { setCommands(prev => { const m = new Map(prev); for (const c of next) m.set(c.id, c); return m; }); return () => { setCommands(prev => { const m = new Map(prev); for (const c of next) m.delete(c.id); return m; }); }; }, []); const unregister = useCallback(ids => { setCommands(prev => { const m = new Map(prev); for (const id of ids) m.delete(id); return m; }); }, []); const clear = useCallback(() => setCommands(new Map()), []); const value = useMemo( () => ({ commands, register, unregister, clear }), [commands, register, unregister, clear], ); return {children}; } function useRegistry(): RegistryState { const ctx = useContext(RegistryContext); if (!ctx) { throw new Error( '@bytelyst/command-palette: useRegisterCommands / useCommands must be used within ', ); } return ctx; } /** * Register commands for the lifetime of the calling component. Returns a * stable function callers can ignore — registration unwinds automatically * on unmount via the returned effect cleanup. * * @example * ```tsx * useRegisterCommands([ * { id: 'new-task', label: 'New task', icon: '+', run: openNewTask }, * { id: 'goto-billing', label: 'Billing', mode: 'navigate', href: '/billing' }, * ]); * ``` */ export function useRegisterCommands(commands: Command[]): void { const { register } = useRegistry(); // Stable ref to the most-recent commands list. We intentionally avoid // re-registering on every keystroke / state tick — callers usually // pass an inline array. The serialization key below keeps it cheap. const key = useMemo( () => commands .map(c => `${c.id}|${c.label}|${c.mode ?? ''}|${c.section ?? ''}`) .join('\n'), [commands], ); useEffect(() => { const off = register(commands); return off; // eslint-disable-next-line react-hooks/exhaustive-deps -- key is the canonical identity }, [key, register]); } /** Read the current registry snapshot. */ export function useCommands(): Command[] { const { commands } = useRegistry(); return useMemo(() => Array.from(commands.values()), [commands]); } /** Imperative access (e.g. for tests or shell-level wiring). */ export function useCommandRegistry(): RegistryState { return useRegistry(); }