{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-prompt",
  "type": "registry:component",
  "title": "Command Prompt",
  "description": "A keyboard-first command prompt with history and autocomplete.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/new-york/blocks/command-prompt/command-prompt.tsx",
      "content": "import * as React from \"react\";\nimport { motion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype Suggestion = {\n  value: string;\n  description?: string;\n  onSelect?: () => void | Promise<void>;\n};\n\nexport type CommandPromptProps = Omit<\n  React.ComponentProps<\"input\">,\n  \"onChange\" | \"value\" | \"defaultValue\"\n> & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  onCommand?: (value: string) => void;\n  suggestions?: Suggestion[];\n  getSuggestions?: (value: string) => Suggestion[];\n  initialHistory?: string[];\n  prefix?: string;\n  frameless?: boolean;\n  showPrefix?: boolean;\n  focusRing?: boolean;\n  background?: \"background\" | \"card\";\n  showActions?: boolean;\n};\n\n/**\n * A keyboard-first command prompt with history navigation and autocomplete.\n * - ArrowUp/ArrowDown to cycle history\n * - Tab to accept highlighted suggestion\n * - Enter to submit\n */\nconst CommandPrompt = React.forwardRef<HTMLInputElement, CommandPromptProps>(function CommandPrompt(\n  {\n    className,\n    value,\n    defaultValue,\n    onValueChange,\n    onCommand,\n    suggestions,\n    getSuggestions,\n    initialHistory,\n    prefix = \">\",\n    disabled,\n    placeholder,\n    showPrefix = true,\n    focusRing = true,\n    background = \"background\",\n    showActions = true,\n    ...inputProps\n  }: CommandPromptProps,\n  ref\n) {\n  const isControlled = value !== undefined;\n  const [uncontrolledValue, setUncontrolledValue] = React.useState<string>(defaultValue ?? \"\");\n  const inputValue = isControlled ? value! : uncontrolledValue;\n  const setInputValue = React.useCallback(\n    (next: string) => {\n      if (disabled) return;\n      if (!isControlled) setUncontrolledValue(next);\n      onValueChange?.(next);\n    },\n    [disabled, isControlled, onValueChange]\n  );\n\n  const [history, setHistory] = React.useState<string[]>(() => initialHistory ?? []);\n  const [historyIndex, setHistoryIndex] = React.useState<number>(-1);\n  const [open, setOpen] = React.useState<boolean>(false);\n  const [activeIndex, setActiveIndex] = React.useState<number>(0);\n  const listRef = React.useRef<HTMLUListElement | null>(null);\n  const inputRef = React.useRef<HTMLInputElement | null>(null);\n  const itemRefs = React.useRef<(HTMLLIElement | null)[]>([]);\n\n  // Expose input ref to parent\n  React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);\n\n  const computedSuggestions: Suggestion[] = React.useMemo(() => {\n    const base = getSuggestions ? getSuggestions(inputValue) : suggestions ?? [];\n    if (!inputValue) return base.slice(0, 8);\n    const lower = inputValue.toLowerCase();\n    return base.filter((s) => s.value.toLowerCase().includes(lower)).slice(0, 8);\n  }, [getSuggestions, suggestions, inputValue]);\n\n  React.useEffect(() => {\n    setOpen(computedSuggestions.length > 0);\n    setActiveIndex(0);\n  }, [computedSuggestions]);\n\n  function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n    if (disabled) return;\n\n    const hasSuggestions = open && computedSuggestions.length > 0;\n\n    if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      if (hasSuggestions) {\n        setActiveIndex(\n          (idx) => (idx - 1 + computedSuggestions.length) % computedSuggestions.length\n        );\n      } else if (history.length > 0) {\n        const nextIndex = historyIndex < 0 ? history.length - 1 : Math.max(0, historyIndex - 1);\n        setHistoryIndex(nextIndex);\n        setInputValue(history[nextIndex] ?? \"\");\n      }\n      return;\n    }\n\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      if (hasSuggestions) {\n        setActiveIndex((idx) => (idx + 1) % computedSuggestions.length);\n      } else if (history.length > 0) {\n        const nextIndex = historyIndex < 0 ? 0 : Math.min(history.length - 1, historyIndex + 1);\n        setHistoryIndex(nextIndex);\n        setInputValue(history[nextIndex] ?? \"\");\n      }\n      return;\n    }\n\n    if (e.key === \"Tab\") {\n      if (open && computedSuggestions[activeIndex]) {\n        e.preventDefault();\n        setInputValue(computedSuggestions[activeIndex].value);\n      }\n      return;\n    }\n\n    if (e.key === \"Enter\") {\n      e.preventDefault();\n      const chosen =\n        open && computedSuggestions[activeIndex]\n          ? computedSuggestions[activeIndex].value\n          : inputValue;\n      const trimmed = chosen.trim();\n      if (open && computedSuggestions[activeIndex]) {\n        computedSuggestions[activeIndex].onSelect?.();\n      }\n      onCommand?.(trimmed);\n      if (trimmed) {\n        setHistory((prev) => (prev[prev.length - 1] === trimmed ? prev : [...prev, trimmed]));\n      }\n      setHistoryIndex(-1);\n      return;\n    }\n\n    if (e.key === \"Escape\") {\n      setOpen(false);\n      setActiveIndex(0);\n      return;\n    }\n  }\n\n  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {\n    setInputValue(e.target.value);\n    setHistoryIndex(-1);\n  }\n\n  function handleMouseMove(idx: number) {\n    setActiveIndex(idx);\n  }\n\n  function handleClickSuggestion(s: Suggestion) {\n    setInputValue(s.value);\n    inputRef.current?.focus();\n    s.onSelect?.();\n    onCommand?.(s.value);\n  }\n\n  const listboxId = React.useId();\n  const activeId = `${listboxId}-option-${activeIndex}`;\n\n  // Keep active option in view\n  React.useEffect(() => {\n    const el = listRef.current?.children?.[activeIndex] as HTMLElement | undefined;\n    if (el) el.scrollIntoView({ block: \"nearest\" });\n  }, [activeIndex]);\n\n  // Background highlight position/size\n  const [highlight, setHighlight] = React.useState<{ top: number; height: number }>({\n    top: 0,\n    height: 0,\n  });\n  React.useEffect(() => {\n    const el = itemRefs.current[activeIndex];\n    if (!el) return;\n    setHighlight({ top: el.offsetTop, height: el.offsetHeight });\n  }, [activeIndex, computedSuggestions.length, open]);\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <div\n        data-slot=\"command-prompt\"\n        className={cn(\n          \"rounded-xl transition-[color,box-shadow]\",\n          background === \"card\" ? \"bg-card\" : \"bg-background\",\n          inputProps.frameless ? \"shadow-none border-none\" : \"border border-input shadow-xl\",\n          focusRing && \"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]\",\n          disabled && \"opacity-50 cursor-not-allowed\"\n        )}\n      >\n        <div className=\"flex w-full items-center gap-2 px-3 pt-3 pb-3\">\n          {showPrefix ? <span className=\"text-muted-foreground select-none\">{prefix}</span> : null}\n          <input\n            ref={inputRef}\n            type=\"text\"\n            aria-autocomplete=\"list\"\n            aria-controls={open ? listboxId : undefined}\n            aria-activedescendant={open ? activeId : undefined}\n            aria-expanded={open}\n            placeholder={placeholder}\n            className={cn(\n              \"placeholder:text-muted-foreground w-full bg-transparent text-base outline-none md:text-sm\"\n            )}\n            value={inputValue}\n            onChange={handleChange}\n            onKeyDown={handleKeyDown}\n            disabled={disabled}\n            {...inputProps}\n          />\n        </div>\n\n        {open && computedSuggestions.length > 0 ? (\n          <ul\n            ref={listRef}\n            id={listboxId}\n            role=\"listbox\"\n            className={cn(\n              \"relative max-h-72 w-full overflow-auto border-t border-input p-1 text-sm\"\n            )}\n          >\n            <motion.div\n              aria-hidden\n              className=\"absolute left-1 right-1 z-0 rounded-md bg-accent\"\n              animate={{ top: highlight.top, height: highlight.height }}\n              transition={{ duration: 0.05, ease: \"easeOut\" }}\n              style={{ top: highlight.top, height: highlight.height }}\n            />\n            {computedSuggestions.map((s, idx) => {\n              const isActive = idx === activeIndex;\n              return (\n                <li\n                  key={`${s.value}-${idx}`}\n                  id={`${listboxId}-option-${idx}`}\n                  role=\"option\"\n                  aria-selected={isActive}\n                  className={cn(\n                    \"relative z-10 flex cursor-pointer items-center justify-between gap-2 rounded-md px-3 py-2 outline-none transition-colors\",\n                    isActive ? \"text-accent-foreground\" : undefined\n                  )}\n                  onMouseMove={() => handleMouseMove(idx)}\n                  onClick={() => handleClickSuggestion(s)}\n                  ref={(el) => {\n                    itemRefs.current[idx] = el;\n                  }}\n                >\n                  <span className=\"truncate\">{s.value}</span>\n                  {s.description ? (\n                    <span className=\"text-muted-foreground hidden shrink-0 text-xs md:inline\">\n                      {s.description}\n                    </span>\n                  ) : null}\n                </li>\n              );\n            })}\n          </ul>\n        ) : null}\n\n        {showActions ? (\n          <div className=\"flex items-center justify-between gap-2 border-t border-input px-3 py-2 text-xs text-muted-foreground\">\n            <div className=\"flex items-center gap-3\">\n              {history.length > 0 ? (\n                <span className=\"flex items-center gap-1.5\">\n                  <kbd className=\"rounded border border-input bg-muted px-1.5 py-0.5 font-mono text-[10px] font-medium\">\n                    ↑↓\n                  </kbd>\n                  <span>history</span>\n                </span>\n              ) : null}\n              <span className=\"flex items-center gap-1.5\">\n                <kbd className=\"rounded border border-input bg-muted px-1.5 py-0.5 font-mono text-[10px] font-medium\">\n                  Esc\n                </kbd>\n                <span>to close</span>\n              </span>\n            </div>\n            <div className=\"flex items-center gap-3\">\n              <span className=\"flex items-center gap-1.5\">\n                <kbd className=\"rounded border border-input bg-muted px-1.5 py-0.5 font-mono text-[10px] font-medium\">\n                  ↵\n                </kbd>\n                <span>to accept</span>\n              </span>\n              {open && computedSuggestions.length > 0 ? (\n                <span className=\"flex items-center gap-1.5\">\n                  <kbd className=\"rounded border border-input bg-muted px-1.5 py-0.5 font-mono text-[10px] font-medium\">\n                    Tab\n                  </kbd>\n                  <span>to complete</span>\n                </span>\n              ) : null}\n            </div>\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n});\n\nexport { CommandPrompt };\n\n// Full-screen overlay with Cmd/Ctrl+K toggle\nexport type CommandPromptOverlayProps = Omit<CommandPromptProps, \"className\"> & {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nfunction useGlobalShortcut(handler: (e: KeyboardEvent) => void) {\n  React.useEffect(() => {\n    function onKey(e: KeyboardEvent) {\n      handler(e);\n    }\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [handler]);\n}\n\nconst CommandPromptOverlay = React.forwardRef<HTMLInputElement, CommandPromptOverlayProps>(\n  function CommandPromptOverlay({ open: controlledOpen, onOpenChange, ...props }, ref) {\n    const [uncontrolledOpen, setUncontrolledOpen] = React.useState<boolean>(false);\n    const isControlled = controlledOpen !== undefined;\n    const open = isControlled ? controlledOpen! : uncontrolledOpen;\n    const setOpen = React.useCallback(\n      (next: boolean) => {\n        if (!isControlled) setUncontrolledOpen(next);\n        onOpenChange?.(next);\n      },\n      [isControlled, onOpenChange]\n    );\n\n    const inputRef = React.useRef<HTMLInputElement | null>(null);\n    React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);\n\n    // Cmd/Ctrl+K to open\n    useGlobalShortcut(\n      React.useCallback(\n        (e: KeyboardEvent) => {\n          const isK = e.key.toLowerCase() === \"k\";\n          if ((e.metaKey || e.ctrlKey) && isK) {\n            e.preventDefault();\n            setOpen(!open);\n          } else if (e.key === \"Escape\" && open) {\n            setOpen(false);\n          }\n        },\n        [open, setOpen]\n      )\n    );\n\n    // Focus input when opened\n    React.useEffect(() => {\n      if (open) {\n        const id = requestAnimationFrame(() => inputRef.current?.focus());\n        return () => cancelAnimationFrame(id);\n      }\n    }, [open]);\n\n    if (!open) return null;\n\n    function handleBackdropClick(e: React.MouseEvent<HTMLDivElement>) {\n      if (e.target === e.currentTarget) setOpen(false);\n    }\n\n    return (\n      <div\n        className={cn(\n          \"fixed inset-0 z-50 grid place-items-center p-4 sm:p-6 bg-background/60 backdrop-blur-xs\"\n        )}\n        onClick={handleBackdropClick}\n      >\n        <div className={cn(\"mx-auto w-full max-w-xl -mt-24\")} role=\"dialog\" aria-modal=\"true\">\n          <CommandPrompt ref={inputRef} {...props} />\n        </div>\n      </div>\n    );\n  }\n);\n\nexport { CommandPromptOverlay };\n",
      "type": "registry:component",
      "target": "components/ui/command-prompt.tsx"
    }
  ]
}