{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-menu",
  "title": "AI Menu",
  "description": "A menu for AI-powered content generation and insertion.",
  "dependencies": [
    "@platejs/ai",
    "@platejs/selection",
    "ai@7",
    "cmdk",
    "@faker-js/faker"
  ],
  "registryDependencies": [
    "command",
    "popover",
    "https://platejs.org/r/use-chat.json",
    "https://platejs.org/r/editor-base-kit.json",
    "https://platejs.org/r/ai-node.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/ai-menu.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport {\n  AIChatPlugin,\n  AIPlugin,\n  useEditorChat,\n  useLastAssistantMessage,\n} from '@platejs/ai/react';\nimport { getTransientCommentKey } from '@platejs/comment';\nimport { BlockSelectionPlugin, useIsSelecting } from '@platejs/selection/react';\nimport { getTransientSuggestionKey } from '@platejs/suggestion';\nimport { Command as CommandPrimitive } from 'cmdk';\nimport {\n  Album,\n  BadgeHelp,\n  BookOpenCheck,\n  Check,\n  CornerUpLeft,\n  FeatherIcon,\n  ListEnd,\n  ListMinus,\n  ListPlus,\n  Loader2Icon,\n  PauseIcon,\n  PenLine,\n  SmileIcon,\n  Wand,\n  X,\n} from 'lucide-react';\nimport {\n  type NodeEntry,\n  type SlateEditor,\n  isHotkey,\n  KEYS,\n  NodeApi,\n  TextApi,\n} from 'platejs';\nimport {\n  useEditorPlugin,\n  useFocusedLast,\n  useHotkeys,\n  usePluginOption,\n} from 'platejs/react';\nimport { type PlateEditor, useEditorRef } from 'platejs/react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  Command,\n  CommandGroup,\n  CommandItem,\n  CommandList,\n} from '@/components/ui/command';\nimport {\n  Popover,\n  PopoverAnchor,\n  PopoverContent,\n} from '@/components/ui/popover';\nimport { cn } from '@/lib/utils';\nimport { commentPlugin } from '@/registry/components/editor/plugins/comment-kit';\n\nimport { AIChatEditor } from './ai-chat-editor';\n\nexport function AIMenu() {\n  const { api, editor } = useEditorPlugin(AIChatPlugin);\n  const mode = usePluginOption(AIChatPlugin, 'mode');\n  const toolName = usePluginOption(AIChatPlugin, 'toolName');\n\n  const streaming = usePluginOption(AIChatPlugin, 'streaming');\n  const isSelecting = useIsSelecting();\n  const isFocusedLast = useFocusedLast();\n  const open = usePluginOption(AIChatPlugin, 'open') && isFocusedLast;\n  const [value, setValue] = React.useState('');\n\n  const [input, setInput] = React.useState('');\n\n  const chat = usePluginOption(AIChatPlugin, 'chat');\n\n  const { messages, status } = chat;\n  const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(\n    null\n  );\n\n  const content = useLastAssistantMessage()?.parts.find(\n    (part) => part.type === 'text'\n  )?.text;\n\n  React.useEffect(() => {\n    if (!streaming) return;\n\n    const anchorEntry = api.aiChat.node({ anchor: true });\n    if (!anchorEntry) return;\n\n    const anchorDom = editor.api.toDOMNode(anchorEntry[0])!;\n    // eslint-disable-next-line react-hooks/set-state-in-effect -- Position the popover from editor DOM while the edit stream is active.\n    setAnchorElement(anchorDom);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [streaming]);\n\n  const setOpen = (open: boolean) => {\n    if (open) {\n      api.aiChat.show();\n    } else {\n      api.aiChat.hide();\n    }\n  };\n\n  const show = (anchorElement: HTMLElement) => {\n    setAnchorElement(anchorElement);\n    setOpen(true);\n  };\n\n  useEditorChat({\n    onOpenBlockSelection: (blocks: NodeEntry[]) => {\n      show(editor.api.toDOMNode(blocks.at(-1)![0])!);\n    },\n    onOpenChange: (open) => {\n      if (!open) {\n        setAnchorElement(null);\n        setInput('');\n      }\n    },\n    onOpenCursor: () => {\n      const [ancestor] = editor.api.block({ highest: true })!;\n\n      if (!editor.api.isAt({ end: true }) && !editor.api.isEmpty(ancestor)) {\n        editor\n          .getApi(BlockSelectionPlugin)\n          .blockSelection.set(ancestor.id as string);\n      }\n\n      show(editor.api.toDOMNode(ancestor)!);\n    },\n    onOpenSelection: () => {\n      show(editor.api.toDOMNode(editor.api.blocks().at(-1)![0])!);\n    },\n  });\n\n  useHotkeys('esc', () => {\n    api.aiChat.stop();\n\n    // remove when you implement the route /api/ai/command\n    (chat as any)._abortFakeStream();\n  });\n\n  const isLoading = status === 'streaming' || status === 'submitted';\n\n  React.useEffect(() => {\n    if (toolName !== 'edit' || mode !== 'chat' || isLoading) return;\n\n    let anchorNode = editor.api.node({\n      at: [],\n      reverse: true,\n      match: (n) => !!n[KEYS.suggestion] && !!n[getTransientSuggestionKey()],\n    });\n\n    if (!anchorNode) {\n      anchorNode = editor\n        .getApi(BlockSelectionPlugin)\n        .blockSelection.getNodes({ selectionFallback: true, sort: true })\n        .at(-1);\n    }\n\n    if (!anchorNode) return;\n\n    const block = editor.api.block({ at: anchorNode[1] });\n    // eslint-disable-next-line react-hooks/set-state-in-effect -- Position the popover from editor DOM after the edit stream completes.\n    setAnchorElement(editor.api.toDOMNode(block![0]!)!);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isLoading]);\n\n  if (isLoading && mode === 'insert') return null;\n\n  if (toolName === 'comment') return null;\n\n  if (toolName === 'edit' && mode === 'chat' && isLoading) return null;\n\n  return (\n    <Popover open={open} onOpenChange={setOpen} modal={false}>\n      <PopoverAnchor virtualRef={{ current: anchorElement! }} />\n\n      <PopoverContent\n        className=\"border-none bg-transparent p-0 shadow-none\"\n        style={{\n          width: anchorElement?.offsetWidth,\n        }}\n        onEscapeKeyDown={(e) => {\n          e.preventDefault();\n\n          api.aiChat.hide();\n        }}\n        align=\"center\"\n        side=\"bottom\"\n      >\n        <Command\n          className=\"w-full rounded-lg border shadow-md\"\n          value={value}\n          onValueChange={setValue}\n        >\n          {mode === 'chat' &&\n            isSelecting &&\n            content &&\n            toolName === 'generate' && <AIChatEditor content={content} />}\n\n          {isLoading ? (\n            <div className=\"flex grow select-none items-center gap-2 p-2 text-muted-foreground text-sm\">\n              <Loader2Icon className=\"size-4 animate-spin\" />\n              {messages.length > 1 ? 'Editing...' : 'Thinking...'}\n            </div>\n          ) : (\n            <CommandPrimitive.Input\n              className={cn(\n                'flex h-9 w-full min-w-0 border-input bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground md:text-sm dark:bg-input/30',\n                'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',\n                'border-b focus-visible:ring-transparent'\n              )}\n              value={input}\n              onKeyDown={(e) => {\n                if (isHotkey('backspace')(e) && input.length === 0) {\n                  e.preventDefault();\n                  api.aiChat.hide();\n                }\n                if (isHotkey('enter')(e) && !e.shiftKey && !value) {\n                  e.preventDefault();\n                  void api.aiChat.submit(input);\n                  setInput('');\n                }\n              }}\n              onValueChange={setInput}\n              placeholder=\"Ask AI anything...\"\n              data-plate-focus\n              autoFocus\n            />\n          )}\n\n          {!isLoading && (\n            <CommandList>\n              <AIMenuItems\n                input={input}\n                setInput={setInput}\n                setValue={setValue}\n              />\n            </CommandList>\n          )}\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\ntype EditorChatState =\n  | 'cursorCommand'\n  | 'cursorSuggestion'\n  | 'selectionCommand'\n  | 'selectionSuggestion';\n\nconst AICommentIcon = () => (\n  <svg\n    fill=\"none\"\n    height=\"24\"\n    stroke=\"currentColor\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    strokeWidth=\"2\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path d=\"M0 0h24v24H0z\" fill=\"none\" stroke=\"none\" />\n    <path d=\"M8 9h8\" />\n    <path d=\"M8 13h4.5\" />\n    <path d=\"M10 19l-1 -1h-3a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v4.5\" />\n    <path d=\"M17.8 20.817l-2.172 1.138a.392 .392 0 0 1 -.568 -.41l.415 -2.411l-1.757 -1.707a.389 .389 0 0 1 .217 -.665l2.428 -.352l1.086 -2.193a.392 .392 0 0 1 .702 0l1.086 2.193l2.428 .352a.39 .39 0 0 1 .217 .665l-1.757 1.707l.414 2.41a.39 .39 0 0 1 -.567 .411l-2.172 -1.138z\" />\n  </svg>\n);\n\nconst aiChatItems = {\n  accept: {\n    icon: <Check />,\n    label: 'Accept',\n    value: 'accept',\n    onSelect: ({ aiEditor, editor }) => {\n      const { mode, toolName } = editor.getOptions(AIChatPlugin);\n\n      if (mode === 'chat' && toolName === 'generate') {\n        return editor\n          .getTransforms(AIChatPlugin)\n          .aiChat.replaceSelection(aiEditor);\n      }\n\n      editor.getTransforms(AIChatPlugin).aiChat.accept();\n      editor.tf.focus({ edge: 'end' });\n    },\n  },\n  comment: {\n    icon: <AICommentIcon />,\n    label: 'Comment',\n    value: 'comment',\n    onSelect: ({ editor, input }) => {\n      editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        mode: 'insert',\n        prompt:\n          'Please comment on the following content and provide reasonable and meaningful feedback.',\n        toolName: 'comment',\n      });\n    },\n  },\n  continueWrite: {\n    icon: <PenLine />,\n    label: 'Continue writing',\n    value: 'continueWrite',\n    onSelect: ({ editor, input }) => {\n      const ancestorNode = editor.api.block({ highest: true });\n\n      if (!ancestorNode) return;\n\n      const isEmpty = NodeApi.string(ancestorNode[0]).trim().length === 0;\n\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        mode: 'insert',\n        prompt: isEmpty\n          ? `<Document>\n{editor}\n</Document>\nStart writing a new paragraph AFTER <Document> ONLY ONE SENTENCE`\n          : 'Continue writing AFTER <Block> ONLY ONE SENTENCE. DONT REPEAT THE TEXT.',\n        toolName: 'generate',\n      });\n    },\n  },\n  discard: {\n    icon: <X />,\n    label: 'Discard',\n    shortcut: 'Escape',\n    value: 'discard',\n    onSelect: ({ editor }) => {\n      editor.getTransforms(AIPlugin).ai.undo();\n      editor.getApi(AIChatPlugin).aiChat.hide();\n    },\n  },\n  emojify: {\n    icon: <SmileIcon />,\n    label: 'Emojify',\n    value: 'emojify',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt:\n          'Add a small number of contextually relevant emojis within each block only. You may insert emojis, but do not remove, replace, or rewrite existing text, and do not modify Markdown syntax, links, or line breaks.',\n        toolName: 'edit',\n      });\n    },\n  },\n  explain: {\n    icon: <BadgeHelp />,\n    label: 'Explain',\n    value: 'explain',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt: {\n          default: 'Explain {editor}',\n          selecting: 'Explain',\n        },\n        toolName: 'generate',\n      });\n    },\n  },\n  fixSpelling: {\n    icon: <Check />,\n    label: 'Fix spelling & grammar',\n    value: 'fixSpelling',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt:\n          'Fix spelling, grammar, and punctuation errors within each block only, without changing meaning, tone, or adding new information.',\n        toolName: 'edit',\n      });\n    },\n  },\n  generateMarkdownSample: {\n    icon: <BookOpenCheck />,\n    label: 'Generate Markdown sample',\n    value: 'generateMarkdownSample',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt: 'Generate a markdown sample',\n        toolName: 'generate',\n      });\n    },\n  },\n  generateMdxSample: {\n    icon: <BookOpenCheck />,\n    label: 'Generate MDX sample',\n    value: 'generateMdxSample',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt: 'Generate a mdx sample',\n        toolName: 'generate',\n      });\n    },\n  },\n  improveWriting: {\n    icon: <Wand />,\n    label: 'Improve writing',\n    value: 'improveWriting',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt:\n          'Improve the writing for clarity and flow, without changing meaning or adding new information.',\n        toolName: 'edit',\n      });\n    },\n  },\n  insertBelow: {\n    icon: <ListEnd />,\n    label: 'Insert below',\n    value: 'insertBelow',\n    onSelect: ({ aiEditor, editor }) => {\n      /** Format: 'none' Fix insert table */\n      void editor\n        .getTransforms(AIChatPlugin)\n        .aiChat.insertBelow(aiEditor, { format: 'none' });\n    },\n  },\n  makeLonger: {\n    icon: <ListPlus />,\n    label: 'Make longer',\n    value: 'makeLonger',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt:\n          'Make the content longer by elaborating on existing ideas within each block only, without changing meaning or adding new information.',\n        toolName: 'edit',\n      });\n    },\n  },\n  makeShorter: {\n    icon: <ListMinus />,\n    label: 'Make shorter',\n    value: 'makeShorter',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt:\n          'Make the content shorter by reducing verbosity within each block only, without changing meaning or removing essential information.',\n        toolName: 'edit',\n      });\n    },\n  },\n  replace: {\n    icon: <Check />,\n    label: 'Replace selection',\n    value: 'replace',\n    onSelect: ({ aiEditor, editor }) => {\n      void editor.getTransforms(AIChatPlugin).aiChat.replaceSelection(aiEditor);\n    },\n  },\n  simplifyLanguage: {\n    icon: <FeatherIcon />,\n    label: 'Simplify language',\n    value: 'simplifyLanguage',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        prompt:\n          'Simplify the language by using clearer and more straightforward wording within each block only, without changing meaning or adding new information.',\n        toolName: 'edit',\n      });\n    },\n  },\n  summarize: {\n    icon: <Album />,\n    label: 'Add a summary',\n    value: 'summarize',\n    onSelect: ({ editor, input }) => {\n      void editor.getApi(AIChatPlugin).aiChat.submit(input, {\n        mode: 'insert',\n        prompt: {\n          default: 'Summarize {editor}',\n          selecting: 'Summarize',\n        },\n        toolName: 'generate',\n      });\n    },\n  },\n  tryAgain: {\n    icon: <CornerUpLeft />,\n    label: 'Try again',\n    value: 'tryAgain',\n    onSelect: ({ editor }) => {\n      void editor.getApi(AIChatPlugin).aiChat.reload();\n    },\n  },\n} satisfies Record<\n  string,\n  {\n    icon: React.ReactNode;\n    label: string;\n    value: string;\n    component?: React.ComponentType<{ menuState: EditorChatState }>;\n    filterItems?: boolean;\n    items?: { label: string; value: string }[];\n    shortcut?: string;\n    onSelect?: ({\n      aiEditor,\n      editor,\n      input,\n    }: {\n      aiEditor: SlateEditor;\n      editor: PlateEditor;\n      input: string;\n    }) => void;\n  }\n>;\n\nconst menuStateItems: Record<\n  EditorChatState,\n  {\n    items: (typeof aiChatItems)[keyof typeof aiChatItems][];\n    heading?: string;\n  }[]\n> = {\n  cursorCommand: [\n    {\n      items: [\n        aiChatItems.comment,\n        aiChatItems.generateMdxSample,\n        aiChatItems.generateMarkdownSample,\n        aiChatItems.continueWrite,\n        aiChatItems.summarize,\n        aiChatItems.explain,\n      ],\n    },\n  ],\n  cursorSuggestion: [\n    {\n      items: [aiChatItems.accept, aiChatItems.discard, aiChatItems.tryAgain],\n    },\n  ],\n  selectionCommand: [\n    {\n      items: [\n        aiChatItems.improveWriting,\n        aiChatItems.comment,\n        aiChatItems.emojify,\n        aiChatItems.makeLonger,\n        aiChatItems.makeShorter,\n        aiChatItems.fixSpelling,\n        aiChatItems.simplifyLanguage,\n      ],\n    },\n  ],\n  selectionSuggestion: [\n    {\n      items: [\n        aiChatItems.accept,\n        aiChatItems.discard,\n        aiChatItems.insertBelow,\n        aiChatItems.tryAgain,\n      ],\n    },\n  ],\n};\n\nexport const AIMenuItems = ({\n  input,\n  setInput,\n  setValue,\n}: {\n  input: string;\n  setInput: (value: string) => void;\n  setValue: (value: string) => void;\n}) => {\n  const editor = useEditorRef();\n  const { messages } = usePluginOption(AIChatPlugin, 'chat');\n  const aiEditor = usePluginOption(AIChatPlugin, 'aiEditor')!;\n  const isSelecting = useIsSelecting();\n\n  const menuState = React.useMemo(() => {\n    if (messages && messages.length > 0) {\n      return isSelecting ? 'selectionSuggestion' : 'cursorSuggestion';\n    }\n\n    return isSelecting ? 'selectionCommand' : 'cursorCommand';\n  }, [isSelecting, messages]);\n\n  const menuGroups = React.useMemo(() => {\n    const items = menuStateItems[menuState];\n\n    return items;\n  }, [menuState]);\n\n  React.useEffect(() => {\n    if (menuGroups.length > 0 && menuGroups[0].items.length > 0) {\n      setValue(menuGroups[0].items[0].value);\n    }\n  }, [menuGroups, setValue]);\n\n  return (\n    <>\n      {menuGroups.map((group, index) => (\n        <CommandGroup key={index} heading={group.heading}>\n          {group.items.map((menuItem) => (\n            <CommandItem\n              key={menuItem.value}\n              className=\"[&_svg]:text-muted-foreground\"\n              value={menuItem.value}\n              onSelect={() => {\n                menuItem.onSelect?.({\n                  aiEditor,\n                  editor,\n                  input,\n                });\n                setInput('');\n              }}\n            >\n              {menuItem.icon}\n              <span>{menuItem.label}</span>\n            </CommandItem>\n          ))}\n        </CommandGroup>\n      ))}\n    </>\n  );\n};\n\nexport function AILoadingBar() {\n  const editor = useEditorRef();\n\n  const toolName = usePluginOption(AIChatPlugin, 'toolName');\n  const chat = usePluginOption(AIChatPlugin, 'chat');\n  const mode = usePluginOption(AIChatPlugin, 'mode');\n\n  const { status } = chat;\n\n  const { api } = useEditorPlugin(AIChatPlugin);\n\n  const isLoading = status === 'streaming' || status === 'submitted';\n\n  const handleComments = (type: 'accept' | 'reject') => {\n    if (type === 'accept') {\n      editor.tf.unsetNodes([getTransientCommentKey()], {\n        at: [],\n        match: (n) => TextApi.isText(n) && !!n[KEYS.comment],\n      });\n    }\n\n    if (type === 'reject') {\n      editor\n        .getTransforms(commentPlugin)\n        .comment.unsetMark({ transient: true });\n    }\n\n    api.aiChat.hide();\n  };\n\n  useHotkeys('esc', () => {\n    api.aiChat.stop();\n\n    // remove when you implement the route /api/ai/command\n    (chat as any)._abortFakeStream();\n  });\n\n  if (\n    isLoading &&\n    (mode === 'insert' ||\n      toolName === 'comment' ||\n      (toolName === 'edit' && mode === 'chat'))\n  ) {\n    return (\n      <div\n        className={cn(\n          '-translate-x-1/2 absolute bottom-4 left-1/2 z-20 flex items-center gap-3 rounded-md border border-border bg-muted px-3 py-1.5 text-muted-foreground text-sm shadow-md transition-all duration-300'\n        )}\n      >\n        <span className=\"h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent\" />\n        <span>{status === 'submitted' ? 'Thinking...' : 'Writing...'}</span>\n        <Button\n          size=\"sm\"\n          variant=\"ghost\"\n          className=\"flex items-center gap-1 text-xs\"\n          onClick={() => api.aiChat.stop()}\n        >\n          <PauseIcon className=\"h-4 w-4\" />\n          Stop\n          <kbd className=\"ml-1 rounded bg-border px-1 font-mono text-[10px] text-muted-foreground shadow-sm\">\n            Esc\n          </kbd>\n        </Button>\n      </div>\n    );\n  }\n\n  if (toolName === 'comment' && status === 'ready') {\n    return (\n      <div\n        className={cn(\n          '-translate-x-1/2 absolute bottom-4 left-1/2 z-50 flex flex-col items-center gap-0 rounded-xl border border-border/50 bg-popover p-1 text-muted-foreground text-sm shadow-xl backdrop-blur-sm',\n          'p-3'\n        )}\n      >\n        {/* Header with controls */}\n        <div className=\"flex w-full items-center justify-between gap-3\">\n          <div className=\"flex items-center gap-5\">\n            <Button\n              size=\"sm\"\n              disabled={isLoading}\n              onClick={() => handleComments('accept')}\n            >\n              Accept\n            </Button>\n\n            <Button\n              size=\"sm\"\n              disabled={isLoading}\n              onClick={() => handleComments('reject')}\n            >\n              Reject\n            </Button>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return null;\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/ui/ai-chat-editor.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport { useAIChatEditor } from '@platejs/ai/react';\nimport { usePlateEditor } from 'platejs/react';\n\nimport { BaseEditorKit } from '@/registry/components/editor/editor-base-kit';\n\nimport { EditorStatic } from './editor-static';\n\nexport const AIChatEditor = React.memo(function AIChatEditor({\n  content,\n}: {\n  content: string;\n}) {\n  const aiEditor = usePlateEditor({\n    plugins: BaseEditorKit,\n  });\n\n  const value = useAIChatEditor(aiEditor, content);\n\n  return <EditorStatic variant=\"aiChat\" editor={aiEditor} value={value} />;\n});\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "docs": [
      {
        "route": "/docs/ai",
        "title": "AI"
      },
      {
        "route": "https://pro.platejs.org/docs/components/ai-menu",
        "title": "AI Menu"
      }
    ],
    "examples": [
      "ai-demo",
      "ai-pro"
    ],
    "label": "New"
  },
  "type": "registry:ui"
}