{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "p-command-2",
  "description": "Command palette with AI assistant",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@coss/autocomplete",
    "@coss/button",
    "@coss/command",
    "@coss/empty",
    "@coss/input",
    "@coss/kbd",
    "@coss/scroll-area",
    "@coss/skeleton",
    "@coss/spinner"
  ],
  "files": [
    {
      "path": "registry/default/particles/p-command-2.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowDownIcon,\n  ArrowLeftIcon,\n  ArrowUpIcon,\n  CircleQuestionMarkIcon,\n  CornerDownLeftIcon,\n  SearchIcon,\n  SparklesIcon,\n} from \"lucide-react\";\nimport Link from \"next/link\";\nimport {\n  Fragment,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { useAutocompleteFilter } from \"@/registry/default/ui/autocomplete\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n  Command,\n  CommandCollection,\n  CommandCreateHandle,\n  CommandDialog,\n  CommandDialogPopup,\n  CommandEmpty,\n  CommandFooter,\n  CommandGroup,\n  CommandGroupLabel,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandPanel,\n  CommandSeparator,\n  CommandShortcut,\n} from \"@/registry/default/ui/command\";\nimport { EmptyMedia } from \"@/registry/default/ui/empty\";\nimport { Input } from \"@/registry/default/ui/input\";\nimport { Kbd, KbdGroup } from \"@/registry/default/ui/kbd\";\nimport { ScrollArea } from \"@/registry/default/ui/scroll-area\";\nimport { Skeleton } from \"@/registry/default/ui/skeleton\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\n\ninterface Item {\n  value: string;\n  label: string;\n  shortcut?: string;\n  keywords?: string[];\n}\n\ninterface Group {\n  value: string;\n  items: Item[];\n}\n\nconst commandGroups: Group[] = [\n  {\n    items: [\n      {\n        keywords: [\"dash\"],\n        label: \"Dashboard\",\n        shortcut: \"d\",\n        value: \"dashboard\",\n      },\n      {\n        keywords: [\"proj\"],\n        label: \"Projects\",\n        shortcut: \"p\",\n        value: \"projects\",\n      },\n      { keywords: [\"team\"], label: \"Team\", shortcut: \"t\", value: \"team\" },\n    ],\n    value: \"Pages\",\n  },\n  {\n    items: [\n      {\n        keywords: [\"prof\"],\n        label: \"Profile\",\n        shortcut: \"p s\",\n        value: \"profile\",\n      },\n      {\n        keywords: [\"acc\"],\n        label: \"Account\",\n        shortcut: \"a s\",\n        value: \"account\",\n      },\n      {\n        keywords: [\"pref\"],\n        label: \"Preferences\",\n        shortcut: \"p r\",\n        value: \"preferences\",\n      },\n    ],\n    value: \"Settings\",\n  },\n  {\n    items: [\n      {\n        keywords: [\"docs\"],\n        label: \"Documentation\",\n        shortcut: \"d o\",\n        value: \"docs\",\n      },\n      {\n        keywords: [\"sup\"],\n        label: \"Support\",\n        shortcut: \"s u\",\n        value: \"support\",\n      },\n      {\n        keywords: [\"feed\"],\n        label: \"Feedback\",\n        shortcut: \"f b\",\n        value: \"feedback\",\n      },\n    ],\n    value: \"Help\",\n  },\n];\n\nconst MOCK_AI_RESPONSE = `To create a new project, navigate to the Projects page and click the \"New Project\" button in the top right corner. You'll be prompted to enter a project name and description.\n\nOnce created, you can invite team members by clicking the \"Share\" button and entering their email addresses. Team members will receive an invitation link via email or you can add them manually by clicking the \"Add Team Member\" button in the project settings.\n\nYou can customize project settings at any time by clicking the settings icon in the project header. For more information, see the Project Settings documentation.`;\n\nconst MOCK_REFERENCE_LINKS = [\n  { title: \"Creating Projects\", url: \"/docs/projects/create\" },\n  { title: \"Team Collaboration\", url: \"/docs/team/collaborate\" },\n  { title: \"Project Settings\", url: \"/docs/projects/settings\" },\n];\n\nexport const commandHandle: ReturnType<typeof CommandCreateHandle> =\n  CommandCreateHandle();\n\ninterface AIState {\n  mode: boolean;\n  query: string;\n  submittedQuery: string;\n  response: string;\n  referenceLinks: Array<{ title: string; url: string }>;\n  isGenerating: boolean;\n  error: string | null;\n}\n\nconst initialAIState: AIState = {\n  error: null,\n  isGenerating: false,\n  mode: false,\n  query: \"\",\n  referenceLinks: [],\n  response: \"\",\n  submittedQuery: \"\",\n};\n\nfunction markdownToSafeHTML(markdown: string): string {\n  // Simple markdown to HTML converter for demo purposes\n  return markdown\n    .split(\"\\n\\n\")\n    .map((para) => `<p>${para}</p>`)\n    .join(\"\");\n}\n\nexport default function PCommand2() {\n  const [open, setOpen] = useState(false);\n  const [aiState, setAIState] = useState<AIState>(initialAIState);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const aiInputRef = useRef<HTMLInputElement>(null);\n  const searchInputRef = useRef<HTMLInputElement>(null);\n  const abortControllerRef = useRef<AbortController | null>(null);\n  const commandResetKeyRef = useRef(0);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      abortControllerRef.current?.abort();\n    };\n  }, []);\n\n  const resetAIState = useCallback(() => {\n    abortControllerRef.current?.abort();\n    setAIState(initialAIState);\n  }, []);\n\n  const handleItemClick = useCallback(() => {\n    setOpen(false);\n  }, []);\n\n  const handleBackToSearch = useCallback(() => {\n    resetAIState();\n    setSearchQuery(\"\");\n    commandResetKeyRef.current += 1;\n    searchInputRef.current?.focus();\n  }, [resetAIState]);\n\n  const handleGenerateAI = useCallback(\n    async (queryOverride?: string) => {\n      const query = queryOverride || aiState.query;\n      if (!query.trim()) return;\n\n      abortControllerRef.current?.abort();\n      const controller = new AbortController();\n      abortControllerRef.current = controller;\n\n      setAIState((prev) => ({\n        ...prev,\n        error: null,\n        isGenerating: true,\n        query: \"\",\n        referenceLinks: [],\n        response: \"\",\n        submittedQuery: query,\n      }));\n\n      try {\n        await new Promise<void>((resolve, reject) => {\n          const timeout = setTimeout(resolve, 1500);\n          controller.signal.addEventListener(\"abort\", () => {\n            clearTimeout(timeout);\n            reject(new Error(\"aborted\"));\n          });\n        });\n\n        if (controller.signal.aborted) return;\n\n        setAIState((prev) => ({\n          ...prev,\n          isGenerating: false,\n          referenceLinks: MOCK_REFERENCE_LINKS,\n          response: MOCK_AI_RESPONSE,\n        }));\n      } catch (error) {\n        if (error instanceof Error && error.message === \"aborted\") {\n          return;\n        }\n\n        if (controller.signal.aborted) return;\n\n        setAIState((prev) => ({\n          ...prev,\n          error: \"Failed to generate response. Please try again.\",\n          isGenerating: false,\n        }));\n      }\n    },\n    [aiState.query],\n  );\n\n  const handleAskAI = useCallback(() => {\n    const currentQuery = searchQuery;\n    setSearchQuery(\"\");\n\n    if (currentQuery.trim()) {\n      setAIState((prev) => ({ ...prev, mode: true }));\n      handleGenerateAI(currentQuery);\n    } else {\n      setAIState((prev) => ({ ...prev, mode: true, query: \"\" }));\n      aiInputRef.current?.focus();\n    }\n  }, [searchQuery, handleGenerateAI]);\n\n  const { contains } = useAutocompleteFilter({ sensitivity: \"base\" });\n\n  const filterItem = useCallback(\n    (itemValue: unknown, query: string): boolean => {\n      if (typeof itemValue !== \"object\" || itemValue === null) {\n        return false;\n      }\n\n      const item = itemValue as Item;\n\n      if (contains(item.label, query)) {\n        return true;\n      }\n\n      if (contains(item.value, query)) {\n        return true;\n      }\n\n      if (item.keywords?.some((keyword) => contains(keyword, query))) {\n        return true;\n      }\n\n      return false;\n    },\n    [contains],\n  );\n\n  useEffect(() => {\n    if (!open || !aiState.mode) return;\n\n    const handleEscape = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        e.preventDefault();\n        e.stopPropagation();\n        handleBackToSearch();\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleEscape, true);\n    return () => document.removeEventListener(\"keydown\", handleEscape, true);\n  }, [open, aiState.mode, handleBackToSearch]);\n\n  useEffect(() => {\n    if (aiState.mode && !aiState.isGenerating) {\n      aiInputRef.current?.focus();\n    }\n  }, [aiState.mode, aiState.isGenerating]);\n\n  const hasResults = useMemo(\n    () =>\n      !searchQuery.trim() ||\n      commandGroups.some((group) =>\n        group.items.some((item) => filterItem(item, searchQuery)),\n      ),\n    [searchQuery, filterItem],\n  );\n\n  const handleOpenChange = useCallback(\n    (newOpen: boolean) => {\n      setOpen(newOpen);\n      if (!newOpen) {\n        setSearchQuery(\"\");\n        resetAIState();\n      }\n    },\n    [resetAIState],\n  );\n\n  return (\n    <>\n      <Button onClick={() => setOpen(true)} variant=\"outline\">\n        Cmdk with AI\n      </Button>\n      <CommandDialog\n        handle={commandHandle}\n        onOpenChange={handleOpenChange}\n        open={open}\n      >\n        <CommandDialogPopup>\n          {!aiState.mode ? (\n            <Command\n              filter={filterItem}\n              items={commandGroups}\n              key={commandResetKeyRef.current}\n            >\n              <div className=\"relative flex items-center *:first:flex-1\">\n                <CommandInput\n                  onChange={(e) => setSearchQuery(e.target.value)}\n                  onKeyDown={(e) => {\n                    if (e.key === \"Tab\") {\n                      e.preventDefault();\n                      handleAskAI();\n                    }\n                    if (\n                      e.key === \"Enter\" &&\n                      !hasResults &&\n                      searchQuery.trim()\n                    ) {\n                      e.preventDefault();\n                      handleAskAI();\n                    }\n                  }}\n                  placeholder=\"Type a command or search...\"\n                  ref={searchInputRef}\n                  value={searchQuery}\n                />\n                <Button\n                  className=\"me-2.5 rounded-md not-hover:text-muted-foreground text-sm sm:text-xs\"\n                  onClick={handleAskAI}\n                  size=\"sm\"\n                  variant=\"ghost\"\n                >\n                  <SparklesIcon className=\"size-4 sm:size-3.5\" />\n                  Ask AI\n                  <Kbd className=\"ms-0.5 -me-1.5\">Tab</Kbd>\n                </Button>\n              </div>\n              <CommandPanel>\n                <CommandEmpty className=\"not-empty:py-12\">\n                  {searchQuery.trim() && (\n                    <div className=\"wrap-break-word flex flex-col flex-wrap items-center gap-2\">\n                      <EmptyMedia variant=\"icon\">\n                        <SearchIcon />\n                      </EmptyMedia>\n                      <p>No results found.</p>\n                      <p>\n                        Press <Kbd>Enter</Kbd> to ask AI about:\n                        <br />{\" \"}\n                        <strong className=\"font-medium text-foreground\">\n                          {searchQuery}\n                        </strong>\n                      </p>\n                    </div>\n                  )}\n                </CommandEmpty>\n                <CommandList>\n                  {(group: Group) => (\n                    <Fragment key={group.value}>\n                      <CommandGroup items={group.items}>\n                        <CommandGroupLabel>{group.value}</CommandGroupLabel>\n                        <CommandCollection>\n                          {(item: Item) => (\n                            <CommandItem\n                              key={item.value}\n                              onClick={handleItemClick}\n                              value={item}\n                            >\n                              <span className=\"flex-1\">{item.label}</span>\n                              {item.shortcut && (\n                                <CommandShortcut>\n                                  {item.shortcut}\n                                </CommandShortcut>\n                              )}\n                            </CommandItem>\n                          )}\n                        </CommandCollection>\n                      </CommandGroup>\n                      <CommandSeparator />\n                    </Fragment>\n                  )}\n                </CommandList>\n              </CommandPanel>\n              <CommandFooter>\n                {hasResults ? (\n                  <>\n                    <div className=\"flex items-center gap-4\">\n                      <div className=\"flex items-center gap-2\">\n                        <KbdGroup>\n                          <Kbd>\n                            <ArrowUpIcon />\n                          </Kbd>\n                          <Kbd>\n                            <ArrowDownIcon />\n                          </Kbd>\n                        </KbdGroup>\n                        <span>Navigate</span>\n                      </div>\n                      <div className=\"flex items-center gap-2\">\n                        <Kbd>\n                          <CornerDownLeftIcon />\n                        </Kbd>\n                        <span>Open</span>\n                      </div>\n                    </div>\n                    <div className=\"flex items-center gap-2\">\n                      <Kbd>Esc</Kbd>\n                      <span>Close</span>\n                    </div>\n                  </>\n                ) : (\n                  <div className=\"ms-auto flex items-center gap-2\">\n                    <Kbd>Esc</Kbd>\n                    <span>Close</span>\n                  </div>\n                )}\n              </CommandFooter>\n            </Command>\n          ) : (\n            <Command>\n              <div className=\"flex items-center *:first:flex-1\">\n                <div className=\"px-2.5 py-1.5\">\n                  <div className=\"relative w-full\">\n                    <div\n                      aria-hidden=\"true\"\n                      className=\"pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5\"\n                      data-slot=\"autocomplete-start-addon\"\n                    >\n                      <SparklesIcon />\n                    </div>\n                    <Input\n                      aria-label=\"AI query input\"\n                      className=\"border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0 *:data-[slot=input]:ps-[calc(--spacing(8.5)-1px)] sm:*:data-[slot=input]:ps-[calc(--spacing(8)-1px)]\"\n                      disabled={aiState.isGenerating}\n                      onChange={(e) =>\n                        setAIState((prev) => ({\n                          ...prev,\n                          query: e.target.value,\n                        }))\n                      }\n                      onKeyDown={(e) => {\n                        if (e.key === \"Enter\" && !aiState.isGenerating) {\n                          handleGenerateAI();\n                        }\n                        if (e.key === \"Escape\") {\n                          e.preventDefault();\n                          handleBackToSearch();\n                        }\n                      }}\n                      placeholder=\"Ask AI anything…\"\n                      ref={aiInputRef}\n                      size=\"lg\"\n                      value={aiState.query}\n                    />\n                  </div>\n                </div>\n                <Button\n                  className=\"me-2.5 rounded-md not-hover:text-muted-foreground text-sm sm:text-xs\"\n                  onClick={handleBackToSearch}\n                  size=\"sm\"\n                  variant=\"ghost\"\n                >\n                  <ArrowLeftIcon className=\"size-4 sm:size-3.5\" />\n                  Back to search\n                  <Kbd className=\"ms-0.5 -me-1.5\">Esc</Kbd>\n                </Button>\n              </div>\n              <CommandPanel>\n                <ScrollArea scrollbarGutter scrollFade>\n                  <div className=\"p-5\">\n                    {!aiState.isGenerating &&\n                      !aiState.response &&\n                      !aiState.error && (\n                        <div className=\"flex items-center justify-center py-12\">\n                          <p className=\"text-muted-foreground text-sm\">\n                            Ask AI anything and press <Kbd>Enter</Kbd> to get\n                            started.\n                          </p>\n                        </div>\n                      )}\n\n                    {aiState.error && (\n                      <div\n                        aria-live=\"polite\"\n                        className=\"text-destructive text-sm\"\n                        role=\"alert\"\n                      >\n                        {aiState.error}\n                      </div>\n                    )}\n\n                    {aiState.isGenerating && (\n                      <div className=\"flex flex-col gap-4\">\n                        <div className=\"flex flex-col gap-2\">\n                          <Skeleton className=\"h-4 w-full\" />\n                          <Skeleton className=\"h-4 w-full\" />\n                          <Skeleton className=\"h-4 w-full\" />\n                          <Skeleton className=\"h-4 w-1/2\" />\n                        </div>\n                        <div className=\"flex flex-col gap-2\">\n                          <Skeleton className=\"h-4 w-full\" />\n                          <Skeleton className=\"h-4 w-full\" />\n                          <Skeleton className=\"h-4 w-3/4\" />\n                        </div>\n                        <div className=\"flex flex-col gap-2\">\n                          <Skeleton className=\"h-4 w-full\" />\n                          <Skeleton className=\"h-4 w-full\" />\n                          <Skeleton className=\"h-4 w-full\" />\n                          <Skeleton className=\"h-4 w-3/5\" />\n                        </div>\n                      </div>\n                    )}\n\n                    {aiState.response && !aiState.isGenerating && (\n                      <>\n                        <div\n                          aria-live=\"polite\"\n                          className=\"text-muted-foreground text-sm **:[a]:underline **:[a]:underline-offset-4 **:[code]:rounded-md **:[code]:bg-muted **:[code]:px-[0.3rem] **:[code]:py-[0.2rem] **:[code]:font-mono **:[p]:not-first:mt-3 **:[p]:leading-relaxed **:[strong,a]:font-medium **:[strong,a]:text-foreground\"\n                          dangerouslySetInnerHTML={{\n                            __html: markdownToSafeHTML(aiState.response),\n                          }}\n                        />\n                        {aiState.referenceLinks.length > 0 && (\n                          <div className=\"mt-4 flex flex-wrap gap-2\">\n                            {aiState.referenceLinks.map((link, index) => (\n                              <Button\n                                key={`${link.url}-${index}`}\n                                render={<Link href={link.url} />}\n                                size=\"sm\"\n                                variant=\"secondary\"\n                              >\n                                {link.title}\n                              </Button>\n                            ))}\n                          </div>\n                        )}\n                      </>\n                    )}\n                  </div>\n                </ScrollArea>\n              </CommandPanel>\n\n              <CommandFooter>\n                {aiState.isGenerating ? (\n                  <div aria-live=\"polite\" className=\"flex items-center gap-2\">\n                    <div className=\"flex h-5 items-center justify-center\">\n                      <Spinner className=\"size-3\" />\n                    </div>\n                    <span className=\"animate-pulse\">Generating response…</span>\n                  </div>\n                ) : aiState.response ? (\n                  <div className=\"flex items-center gap-2\">\n                    <div className=\"flex h-5 items-center justify-center\">\n                      <CircleQuestionMarkIcon className=\"size-3\" />\n                    </div>\n                    You asked: <span>&quot;{aiState.submittedQuery}&quot;</span>\n                  </div>\n                ) : (\n                  <div className=\"flex items-center gap-2\">\n                    <Kbd>\n                      <CornerDownLeftIcon />\n                    </Kbd>\n                    <span>Ask AI</span>\n                  </div>\n                )}\n              </CommandFooter>\n            </Command>\n          )}\n        </CommandDialogPopup>\n      </CommandDialog>\n    </>\n  );\n}\n",
      "type": "registry:block"
    }
  ],
  "categories": [
    "command",
    "dialog"
  ],
  "type": "registry:block"
}