{
  "name": "command",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "command.tsx",
      "content": "\"use client\";\r\n\r\nimport { css, fadeIn, fadeOut, scaleIn, scaleOut, themeVars as theme } from \"@yugnex/core\";\r\nimport { useEscapeKey, usePortal, usePresence } from \"@yugnex/core/client\";\r\nimport { useEffect, useId, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\r\nimport { createPortal } from \"react-dom\";\r\n\r\nconst overlayClass = css({\r\n  position: \"fixed\",\r\n  inset: 0,\r\n  zIndex: theme.zIndex.overlay,\r\n  backgroundColor: theme.color.overlay,\r\n  '&[data-state=\"open\"]': { animation: `${fadeIn} ${theme.duration.base} ${theme.easing.standard}` },\r\n  '&[data-state=\"closed\"]': { animation: `${fadeOut} ${theme.duration.base} ${theme.easing.standard}` },\r\n});\r\n\r\nconst panelClass = css({\r\n  position: \"fixed\",\r\n  top: \"18%\",\r\n  left: \"50%\",\r\n  transform: \"translateX(-50%)\",\r\n  zIndex: theme.zIndex.modal,\r\n  width: \"min(34rem, calc(100vw - 2rem))\",\r\n  borderRadius: theme.radius.lg,\r\n  border: `1px solid ${theme.color.border}`,\r\n  backgroundColor: theme.color.popover,\r\n  color: theme.color.popoverForeground,\r\n  boxShadow: theme.shadow.overlay,\r\n  overflow: \"hidden\",\r\n  fontFamily: theme.fontFamily.sans,\r\n  '&[data-state=\"open\"]': { animation: `${scaleIn} ${theme.duration.base} ${theme.easing.decelerate}` },\r\n  '&[data-state=\"closed\"]': { animation: `${scaleOut} ${theme.duration.fast} ${theme.easing.accelerate}` },\r\n});\r\n\r\nconst inputRowClass = css({\r\n  display: \"flex\",\r\n  alignItems: \"center\",\r\n  gap: theme.space[2.5],\r\n  padding: `${theme.space[3]} ${theme.space[4]}`,\r\n  borderBottom: `1px solid ${theme.color.border}`,\r\n});\r\n\r\nconst inputClass = css({\r\n  flex: 1,\r\n  border: \"none\",\r\n  outline: \"none\",\r\n  background: \"transparent\",\r\n  color: theme.color.foreground,\r\n  fontFamily: theme.fontFamily.sans,\r\n  fontSize: theme.fontSize.base,\r\n  \"&::placeholder\": { color: theme.color.mutedForeground },\r\n});\r\n\r\nconst listClass = css({\r\n  maxHeight: \"20rem\",\r\n  overflowY: \"auto\",\r\n  padding: theme.space[1.5],\r\n  margin: 0,\r\n  listStyle: \"none\",\r\n});\r\n\r\nconst groupLabelClass = css({\r\n  padding: `${theme.space[2]} ${theme.space[2.5]} ${theme.space[1]}`,\r\n  fontSize: theme.fontSize.xs,\r\n  fontWeight: theme.fontWeight.semibold,\r\n  letterSpacing: theme.letterSpacing.wide,\r\n  textTransform: \"uppercase\",\r\n  color: theme.color.mutedForeground,\r\n});\r\n\r\nconst itemClass = css({\r\n  display: \"flex\",\r\n  alignItems: \"center\",\r\n  gap: theme.space[2.5],\r\n  padding: `${theme.space[2]} ${theme.space[2.5]}`,\r\n  borderRadius: theme.radius.sm,\r\n  cursor: \"pointer\",\r\n  fontSize: theme.fontSize.sm,\r\n  '&[data-active=\"true\"]': {\r\n    backgroundColor: theme.color.accent,\r\n    color: theme.color.accentForeground,\r\n  },\r\n});\r\n\r\nconst itemIconClass = css({ flexShrink: 0, display: \"inline-flex\", color: theme.color.mutedForeground });\r\nconst itemLabelClass = css({ flex: 1, minWidth: 0 });\r\nconst itemHintClass = css({ flexShrink: 0, fontSize: theme.fontSize.xs, color: theme.color.mutedForeground });\r\n\r\nconst emptyClass = css({\r\n  padding: `${theme.space[8]} ${theme.space[4]}`,\r\n  textAlign: \"center\",\r\n  fontSize: theme.fontSize.sm,\r\n  color: theme.color.mutedForeground,\r\n});\r\n\r\nconst footerClass = css({\r\n  display: \"flex\",\r\n  alignItems: \"center\",\r\n  gap: theme.space[3],\r\n  padding: `${theme.space[2]} ${theme.space[4]}`,\r\n  borderTop: `1px solid ${theme.color.border}`,\r\n  fontSize: theme.fontSize.xs,\r\n  color: theme.color.mutedForeground,\r\n});\r\n\r\nexport interface CommandItem {\r\n  id: string;\r\n  label: string;\r\n  /** Extra words matched by the filter but not displayed — aliases, synonyms. */\r\n  keywords?: string;\r\n  group?: string;\r\n  icon?: ReactNode;\r\n  /** Right-aligned hint, typically a shortcut. */\r\n  hint?: ReactNode;\r\n  onSelect?: () => void;\r\n}\r\n\r\nexport interface CommandProps {\r\n  open: boolean;\r\n  onOpenChange: (open: boolean) => void;\r\n  items: CommandItem[];\r\n  placeholder?: string;\r\n  emptyMessage?: string;\r\n  footer?: ReactNode;\r\n}\r\n\r\n/**\r\n * A ⌘K command palette: fuzzy-ish substring filtering over label + keywords,\r\n * arrow/Enter navigation, and grouping.\r\n *\r\n * Uses the ARIA combobox pattern — focus stays in the text input while\r\n * `aria-activedescendant` points at the highlighted row, so typing and\r\n * navigating never fight over focus.\r\n */\r\nexport function Command({\r\n  open,\r\n  onOpenChange,\r\n  items,\r\n  placeholder = \"Type a command or search…\",\r\n  emptyMessage = \"No results found.\",\r\n  footer,\r\n}: CommandProps) {\r\n  const [query, setQuery] = useState(\"\");\r\n  const [activeIndex, setActiveIndex] = useState(0);\r\n  const { mounted, dataState } = usePresence(open, { exitDuration: 150 });\r\n  const portalNode = usePortal();\r\n  const inputRef = useRef<HTMLInputElement | null>(null);\r\n  const baseId = useId();\r\n\r\n  useEscapeKey(() => onOpenChange(false), open);\r\n\r\n  const filtered = useMemo(() => {\r\n    const q = query.trim().toLowerCase();\r\n    if (!q) return items;\r\n    return items.filter((item) => `${item.label} ${item.keywords ?? \"\"}`.toLowerCase().includes(q));\r\n  }, [items, query]);\r\n\r\n  // Reset the query and highlight each time the palette opens.\r\n  useEffect(() => {\r\n    if (!open) return;\r\n    setQuery(\"\");\r\n    setActiveIndex(0);\r\n  }, [open]);\r\n\r\n  // Focus is keyed on `mounted`, not `open`: the portaled input does not exist\r\n  // until a tick after `open` flips, so focusing on `open` would run against a\r\n  // null ref and silently leave focus on whatever opened the palette.\r\n  useEffect(() => {\r\n    if (!mounted) return;\r\n    const raf = requestAnimationFrame(() => inputRef.current?.focus());\r\n    return () => cancelAnimationFrame(raf);\r\n  }, [mounted]);\r\n\r\n  // A narrowing filter can strand the highlight past the end of the list.\r\n  useEffect(() => {\r\n    setActiveIndex((current) => (current >= filtered.length ? 0 : current));\r\n  }, [filtered.length]);\r\n\r\n  useEffect(() => {\r\n    if (!mounted) return;\r\n    document.getElementById(`${baseId}-item-${activeIndex}`)?.scrollIntoView({ block: \"nearest\" });\r\n  }, [mounted, activeIndex, baseId]);\r\n\r\n  useEffect(() => {\r\n    if (!mounted) return;\r\n    const original = document.body.style.overflow;\r\n    document.body.style.overflow = \"hidden\";\r\n    return () => {\r\n      document.body.style.overflow = original;\r\n    };\r\n  }, [mounted]);\r\n\r\n  function run(index: number) {\r\n    const item = filtered[index];\r\n    if (!item) return;\r\n    onOpenChange(false);\r\n    item.onSelect?.();\r\n  }\r\n\r\n  function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {\r\n    if (event.key === \"ArrowDown\") {\r\n      event.preventDefault();\r\n      setActiveIndex((i) => (filtered.length === 0 ? 0 : (i + 1) % filtered.length));\r\n    } else if (event.key === \"ArrowUp\") {\r\n      event.preventDefault();\r\n      setActiveIndex((i) => (filtered.length === 0 ? 0 : (i - 1 + filtered.length) % filtered.length));\r\n    } else if (event.key === \"Home\") {\r\n      event.preventDefault();\r\n      setActiveIndex(0);\r\n    } else if (event.key === \"End\") {\r\n      event.preventDefault();\r\n      setActiveIndex(Math.max(filtered.length - 1, 0));\r\n    } else if (event.key === \"Enter\") {\r\n      event.preventDefault();\r\n      run(activeIndex);\r\n    }\r\n  }\r\n\r\n  if (!mounted || !portalNode) return null;\r\n\r\n  const seenGroups = new Set<string>();\r\n\r\n  return createPortal(\r\n    <>\r\n      <div className={overlayClass} data-state={dataState} aria-hidden=\"true\" onClick={() => onOpenChange(false)} />\r\n      <div className={panelClass} data-state={dataState} role=\"dialog\" aria-modal=\"true\" aria-label=\"Command palette\">\r\n        <div className={inputRowClass}>\r\n          <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" style={{ color: theme.color.mutedForeground }}>\r\n            <circle cx=\"7\" cy=\"7\" r=\"5\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\r\n            <path d=\"M11 11L14 14\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\r\n          </svg>\r\n          <input\r\n            ref={inputRef}\r\n            className={inputClass}\r\n            placeholder={placeholder}\r\n            value={query}\r\n            role=\"combobox\"\r\n            aria-expanded\r\n            aria-controls={`${baseId}-list`}\r\n            aria-activedescendant={filtered.length ? `${baseId}-item-${activeIndex}` : undefined}\r\n            aria-autocomplete=\"list\"\r\n            onChange={(event) => setQuery(event.target.value)}\r\n            onKeyDown={handleKeyDown}\r\n          />\r\n        </div>\r\n\r\n        {filtered.length === 0 ? (\r\n          <p className={emptyClass}>{emptyMessage}</p>\r\n        ) : (\r\n          <ul className={listClass} id={`${baseId}-list`} role=\"listbox\" aria-label=\"Commands\">\r\n            {filtered.map((item, index) => {\r\n              const showGroup = item.group && !seenGroups.has(item.group);\r\n              if (item.group) seenGroups.add(item.group);\r\n              return (\r\n                <li key={item.id} style={{ listStyle: \"none\" }}>\r\n                  {showGroup ? (\r\n                    <div className={groupLabelClass} role=\"presentation\">\r\n                      {item.group}\r\n                    </div>\r\n                  ) : null}\r\n                  <div\r\n                    id={`${baseId}-item-${index}`}\r\n                    role=\"option\"\r\n                    aria-selected={index === activeIndex}\r\n                    data-active={index === activeIndex}\r\n                    className={itemClass}\r\n                    onClick={() => run(index)}\r\n                    onMouseEnter={() => setActiveIndex(index)}\r\n                  >\r\n                    {item.icon ? <span className={itemIconClass}>{item.icon}</span> : null}\r\n                    <span className={itemLabelClass}>{item.label}</span>\r\n                    {item.hint ? <span className={itemHintClass}>{item.hint}</span> : null}\r\n                  </div>\r\n                </li>\r\n              );\r\n            })}\r\n          </ul>\r\n        )}\r\n\r\n        {footer ? <div className={footerClass}>{footer}</div> : null}\r\n      </div>\r\n    </>,\r\n    portalNode,\r\n  );\r\n}\r\n\r\n/** Opens a <Command> on ⌘K / Ctrl+K. Call at the app root alongside the palette. */\r\nexport function useCommandShortcut(onOpen: () => void, key = \"k\"): void {\r\n  const handlerRef = useRef(onOpen);\r\n  handlerRef.current = onOpen;\r\n\r\n  useEffect(() => {\r\n    const listener = (event: globalThis.KeyboardEvent) => {\r\n      if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === key) {\r\n        event.preventDefault();\r\n        handlerRef.current();\r\n      }\r\n    };\r\n    document.addEventListener(\"keydown\", listener);\r\n    return () => document.removeEventListener(\"keydown\", listener);\r\n  }, [key]);\r\n}\r\n",
      "type": "registry:component"
    }
  ]
}