Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Comments
  • Discussion
  • Suggestions
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Details
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Combobox

PreviousNext

Utilities for adding combobox to your editor.

Inline Combobox

Mention

Insert mentions for users, pages, or any reference with @

Slash Command

Quick access to editor commands and blocks with /

Emoji

Insert emojis with autocomplete using :

Features

  • Autocomplete over ordinary editor text, with the trigger and query typed into the document
  • A popup opens only when the user types the trigger in that editor
  • Completion replaces the trigger and query in one undo step; undo restores the typed text
Report an issue
Block PlaceholderEmoji

On This Page

FeaturesCreate a combobox pluginInstallationCreate the pluginCreate the popupAdd to editorExamplesBehaviorOptionsComboboxStateHooksuseComboboxRegistry UI
Build your editor
Production-ready AI template and reusable components.
Get all-access
  • IME preedit filters options without selecting one
  • Escape closes the popup and keeps the text
  • Create a combobox plugin

    Installation

    pnpm add platejs
    npx shadcn@latest add @plate/inline-combobox
    pnpm add platejs
    npx shadcn@latest add @plate/inline-combobox

    Create the plugin

    Keep the trigger policy in the plugin state and the inserted node in the plugin schema:

    import { property } from 'platejs';
    import type { ComboboxState } from 'platejs/combobox';
    import { definePlugin } from 'platejs/react';
     
    export const HashtagPlugin = definePlugin('hashtag', {
      initialState: (): ComboboxState => ({
        maxQueryLength: 50,
        queryPattern: /^[\p{L}\p{N}_-]$/u,
        trigger: '#',
        triggerPreviousCharPattern: /^\s?$/,
        triggerQuery: null,
    
    
    
    
    
    
    
    
    
    • trigger: Text that opens the popup when typed (#).
    • triggerPreviousCharPattern: Must match the character before the trigger. /^\s?$/ allows the start of a line or a space.
    • queryPattern: Tested against each query character. A space ends a hashtag query here.

    Create the popup

    The popup renders next to the editor and reads the query from the text after the trigger:

    import * as React from 'react';
    import {
      type EditorElementProps,
      EditorElement,
    } from 'platejs/react';
    import {
      InlineCombobox,
      InlineComboboxContent,
      InlineComboboxEmpty,
      InlineComboboxItem,
    } from '@/components/editor/inline-combobox';
    import { HashtagPlugin } from './hashtag-plugin';
     
    const tags = ['Frontend', 'Backend', 'Design', 'Urgent'];
     
    export function HashtagCombobox({
      editableRef,
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    Add to editor

    Mount the popup in the plugin's afterEditable slot, which receives each editor's editableRef:

    import { createEditor } from 'platejs/react';
    import { HashtagPlugin } from './hashtag-plugin';
    import { HashtagCombobox, HashtagElement } from './hashtag-components';
     
    const editor = createEditor({
      plugins: [
        HashtagPlugin.configure({
          component: HashtagElement,
          slots: { afterEditable: HashtagCombobox },
        }),
      ],
    });
    import
    
    
    
    
    
    
    
    
    
    
    

    Examples

    Loading…
    Loading…
    Loading…

    Behavior

    • The popup opens when the user types the trigger. Moving the caret after existing trigger text, undo, paste and remote edits never open it.
    • The query is the text from the trigger to the caret, across marks, inside one block. It ends when the caret moves before the trigger or past the typed text, a character fails queryPattern, it starts with whitespace or it grows past maxQueryLength.
    • Escape closes the popup and leaves the typed text. Typing more does not reopen it; typing the trigger again does.
    • Completion deletes the trigger and query and runs your insertion in one transaction. Undo restores the typed text.
    • While an IME composes, the query previews the visible preedit. Enter confirms the composition and never selects an option.

    Options

    ComboboxState

    Trigger policy kept in a feature plugin's state. Override it with .configure({ initialState }).

    Options

      Text that opens the popup when typed. Strings may be longer than one character, such as '[^'. A pattern is tested against the typed character.

      Tested against the character before the trigger, or '' at the start of a text. null allows any character.

      Return false to keep a typed trigger as plain text, for example inside code.

      Tested against each query character; the first failure closes the popup. null allows any character except a line break.

      The popup closes once the query is longer.

    Hooks

    useCombobox

    Connects a popup to one editor. Import it from platejs/combobox/react and call it inside an afterEditable slot component.

    While a typed trigger's query is open, the editor reports itself as a combobox. It gets role="combobox", aria-haspopup="listbox", aria-autocomplete="list" and aria-expanded, and drops aria-multiline. It gets its own attributes back when the query closes.

    Parameters

      The editor the popup serves, as passed to the slot.

      The feature plugin whose state holds the ComboboxState.

      The active option's id, mirrored as the editor's aria-activedescendant.

      Whether the popup is showing. Keys, aria-controls and aria-activedescendant attach only while it shows, and aria-expanded follows it, so a popup hidden with no results leaves arrows and Escape to the editor.

      • Default: true

      Receives Enter, Tab, ArrowUp, ArrowDown and Escape while the popup is open, before editor shortcuts. Return true when handled. An unhandled Escape closes the popup.

    Returns

      The open query, or null while closed. match.query holds the text after the trigger, match.range covers the trigger and query, and match.composing is true during IME preedit.

      Id for the popup's listbox, mirrored as the editor's aria-controls.

      Replace the trigger and query through one synchronous transaction. Pass the match that offered the option. Return false from the callback to refuse and keep the query. A match from an earlier query, a newer query, an active composition or a refused callback changes nothing and returns false; a thrown callback rolls back everything.

      Close the popup and keep the typed text.

    Registry UI

    The copied inline-combobox component wraps useCombobox with an Ariakit listbox. It owns filtering, the active option and the popup position at the trigger. Insert the chosen feature in InlineComboboxItem.onSelect, using its inferred transaction, and return false when the option is no longer available. Read the query inside the popup with useInlineComboboxQuery(). Use onClick for a UI action after a successful completion, such as opening another popover.

    'use client';
     
    import {
      ComboboxGroup,
      ComboboxGroupLabel,
      ComboboxItem,
      ComboboxPopover,
      ComboboxProvider,
      Portal,
      useComboboxContext,
      useComboboxStore,
      useStoreState,
    } from '@ariakit/react';
    import { cva } from 'class-variance-authority';
    import type { PluginReference, PluginTransaction } from 'platejs';
    import { filterWords } from 'platejs/combobox';
    import {
      type ComboboxMatch,
      type UseComboboxOptions,
      type UseComboboxReturn,
      useCombobox,
    } from 'platejs/combobox/react';
    import { useEditor } from 'platejs/react';
    import * as React from 'react';
     
    import { cn } from '@/lib/utils';
     
    type FilterFn = (
      item: { value: string; group?: string; keywords?: string[]; label?: string },
      search: string
    ) => boolean;
     
    type InlineComboboxContextValue = {
      box: UseComboboxReturn;
      filter: FilterFn | false;
      match: ComboboxMatch | null;
      setHasEmpty: (hasEmpty: boolean) => void;
    };
     
    const InlineComboboxContext =
      React.createContext<InlineComboboxContextValue | null>(null);
     
    const useInlineComboboxContext = () => {
      const context = React.useContext(InlineComboboxContext);
     
      if (!context) {
        throw new Error('Inline combobox components require InlineCombobox');
      }
     
      return context;
    };
     
    /** The text typed after the trigger, or `''` when no trigger is active. */
    const useInlineComboboxQuery = () =>
      useInlineComboboxContext().match?.query ?? '';
     
    const defaultFilter: FilterFn = (
      { group, keywords = [], label, value },
      search
    ) => {
      const uniqueTerms = new Set(
        [value, ...keywords, group, label].flatMap((term) =>
          typeof term === 'string' ? [term] : []
        )
      );
     
      return Array.from(uniqueTerms).some((keyword) =>
        filterWords(keyword, search)
      );
    };
     
    const InlineCombobox = <P extends PluginReference>({
      children,
      editableRef,
      filter = defaultFilter,
      hideWhenNoValue = false,
      plugin,
    }: {
      children: React.ReactNode;
      editableRef: React.RefObject<HTMLDivElement | null>;
      plugin: UseComboboxOptions<P>['plugin'];
      filter?: FilterFn | false;
      hideWhenNoValue?: boolean;
    }) => {
      // Without a text input, a null active id would point at no option.
      const store = useComboboxStore({ includesBaseElement: false });
      const activeId = useStoreState(store, 'activeId');
      const items = useStoreState(store, 'items');
      const moves = useStoreState(store, 'moves');
      const renderedItems = useStoreState(store, 'renderedItems');
      const onKeyDown = React.useCallback(
        (event: React.KeyboardEvent<HTMLDivElement>) => {
          if (store.getState().renderedItems.length === 0) return false;
          if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
            const down = event.key === 'ArrowDown';
            const next = down ? store.next() : store.previous();
     
            store.move(next ?? (down ? store.first() : store.last()));
     
            return true;
          }
          if (event.key !== 'Enter' && event.key !== 'Tab') return false;
     
          const { activeId: currentId, items: options } = store.getState();
          const option = (
            options.find((item) => item.id === currentId) ??
            options.find((item) => item.id === store.first())
          )?.element;
     
          if (!option) return false;
     
          option.click();
     
          return true;
        },
        [store]
      );
      const [shown, setShown] = React.useState(false);
      const box = useCombobox({
        activeOptionId: activeId ?? null,
        editableRef,
        onKeyDown,
        open: shown,
        plugin,
      });
      const { dismiss, match } = box;
      const query = match?.query ?? '';
      const [hasEmpty, setHasEmpty] = React.useState(false);
      const open =
        !!match &&
        (items.length > 0 || hasEmpty) &&
        (!hideWhenNoValue || query.length > 0);
     
      if (shown !== open) setShown(open);
     
      React.useEffect(() => {
        store.setAnchorElement(editableRef.current);
      }, [editableRef, store]);
     
      React.useEffect(() => {
        const { activeId: id } = store.getState();
     
        if (!id) {
          store.setActiveId(store.first());
        } else if (!renderedItems.some((item) => item.id === id)) {
          // A move scrolls the first option back into view; opening stays still.
          store.move(store.first());
        }
      }, [renderedItems, store]);
     
      // Ariakit scrolls on move only from its own Combobox input, which the
      // editor replaces. Scroll without focusing so the editor keeps focus.
      React.useEffect(() => {
        if (!moves) return;
     
        const { activeId: id } = store.getState();
     
        store.item(id)?.element?.scrollIntoView({
          block: 'nearest',
          inline: 'nearest',
        });
      }, [moves, store]);
     
      const queryBecameProse =
        !!match && !match.composing && items.length === 0 && /\s$/.test(query);
     
      React.useEffect(() => {
        if (queryBecameProse) dismiss();
      }, [dismiss, queryBecameProse]);
     
      const contextValue = React.useMemo<InlineComboboxContextValue>(
        () => ({ box, filter, match, setHasEmpty }),
        [box, filter, match]
      );
     
      return (
        <ComboboxProvider
          open={open}
          setOpen={(nextOpen) => {
            if (!nextOpen) dismiss();
          }}
          store={store}
          value={query}
        >
          <InlineComboboxContext value={contextValue}>
            {children}
          </InlineComboboxContext>
        </ComboboxProvider>
      );
    };
     
    const InlineComboboxContent = ({
      className,
      ...props
    }: React.HTMLAttributes<HTMLDivElement>) => {
      const editor = useEditor();
      const { box, match } = useInlineComboboxContext();
      const getAnchorRect = React.useCallback(() => {
        if (!match) return null;
     
        const start = match.range.anchor;
     
        return editor.api.dom.resolveRangeRect({ anchor: start, focus: start });
      }, [editor, match]);
     
      return (
        <Portal>
          <ComboboxPopover
            id={box.listboxId}
            className={cn(
              'cn-command cn-command-list z-500 w-[300px] overflow-x-hidden overflow-y-auto shadow-md',
              className
            )}
            autoFocusOnHide={false}
            autoFocusOnShow={false}
            getAnchorRect={getAnchorRect}
            hideOnEscape={false}
            hideOnInteractOutside={(event) =>
              !(event.target instanceof Node) ||
              !editor.api.dom.root()?.contains(event.target)
            }
            modal={false}
            // Keep the editor focused unless an IME is composing: the blur ends it.
            onMouseDown={(event) => {
              if (!match?.composing) event.preventDefault();
            }}
            {...props}
          />
        </Portal>
      );
    };
     
    const comboboxItemVariants = cva(
      'cn-command-item mx-1 h-7 cursor-pointer text-foreground transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground'
    );
     
    const InlineComboboxItem = ({
      className,
      focusEditor = true,
      group,
      keywords,
      label,
      onClick,
      onSelect,
      ...props
    }: Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect' | 'value'> & {
      focusEditor?: boolean;
      group?: string;
      keywords?: string[];
      label?: string;
      value: string;
      /** Insert the completion. Return false to refuse and keep the query. */
      onSelect?: (tx: PluginTransaction) => false | void;
    }) => {
      const { value } = props;
      const editor = useEditor();
      const { box, filter, match } = useInlineComboboxContext();
      const search = match?.query ?? '';
      const visible = React.useMemo(
        () => !filter || filter({ group, keywords, label, value }, search),
        [filter, group, keywords, label, value, search]
      );
     
      if (!visible || !match) return null;
     
      return (
        <ComboboxItem
          className={cn(comboboxItemVariants(), className)}
          hideOnClick={false}
          setValueOnClick={false}
          onClick={(event) => {
            if (!box.complete(match, (tx) => onSelect?.(tx))) return;
            if (focusEditor) editor.api.dom.focus();
            onClick?.(event);
          }}
          {...props}
        />
      );
    };
     
    const InlineComboboxEmpty = ({
      children,
      className,
    }: React.HTMLAttributes<HTMLDivElement>) => {
      const { setHasEmpty } = useInlineComboboxContext();
      const store = useComboboxContext();
     
      if (store == null) {
        throw new Error('InlineComboboxEmpty requires a Combobox store');
      }
     
      const items = useStoreState(store, 'items');
     
      React.useEffect(() => {
        setHasEmpty(true);
     
        return () => {
          setHasEmpty(false);
        };
      }, [setHasEmpty]);
     
      if (items.length > 0) return null;
     
      return <div className={cn('cn-command-empty', className)}>{children}</div>;
    };
     
    function InlineComboboxGroup({
      className,
      ...props
    }: React.HTMLAttributes<HTMLDivElement>) {
      return (
        <ComboboxGroup
          {...props}
          className={cn(
            'cn-command-group hidden not-last:border-b [&:has([role=option])]:block',
            className
          )}
        />
      );
    }
     
    function InlineComboboxGroupLabel({
      className,
      ...props
    }: React.HTMLAttributes<HTMLDivElement>) {
      return (
        <ComboboxGroupLabel
          {...props}
          className={cn(
            'mt-1.5 mb-2 px-3 font-medium text-muted-foreground text-xs',
            className
          )}
        />
      );
    }
     
    export {
      InlineCombobox,
      InlineComboboxContent,
      InlineComboboxEmpty,
      InlineComboboxGroup,
      InlineComboboxGroupLabel,
      InlineComboboxItem,
      useInlineComboboxQuery,
    };
    'use client';
     
    import {
      ComboboxGroup,
      ComboboxGroupLabel,
      ComboboxItem,
      ComboboxPopover,
      ComboboxProvider,
      Portal,
      useComboboxContext,
      useComboboxStore,
      useStoreState,
    } from '@ariakit/react';
    import { cva } from 'class-variance-authority';
    import type { PluginReference, PluginTransaction } from 'platejs';
    import { filterWords } from 'platejs/combobox';
    import {
      type ComboboxMatch,
      type UseComboboxOptions,
      type UseComboboxReturn,
      useCombobox,
    } from
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    }),
    schema: {
    element: {
    properties: { value: property.string({ required: true }) },
    type: 'hashtag',
    void: 'inline',
    },
    },
    });
    import { property } from 'platejs';
    import type { ComboboxState } from 'platejs/combobox';
    import { definePlugin } from 'platejs/react';
     
    export const HashtagPlugin = definePlugin('hashtag', {
      initialState: (): ComboboxState => ({
        maxQueryLength: 50,
        queryPattern: /^[\p{L}\p{N}_-]$/u,
        trigger: '#',
        triggerPreviousCharPattern: /^\s?$/,
        triggerQuery: null,
      }),
      schema: {
        element: {
          properties: { value: property.string({ required: true }) },
          type: 'hashtag',
          void: 'inline',
        },
      },
    });
    }
    :
    {
    editableRef: React.RefObject<HTMLDivElement | null>;
    }) {
    return (
    <InlineCombobox editableRef={editableRef} plugin={HashtagPlugin}>
    <InlineComboboxContent>
    <InlineComboboxEmpty>No tags found</InlineComboboxEmpty>
    {tags.map((tag) => (
    <InlineComboboxItem
    key={tag}
    value={tag}
    onSelect={(tx) => {
    tx.plugin(HashtagPlugin).insert({ value: tag });
    }}
    >
    #{tag}
    </InlineComboboxItem>
    ))}
    </InlineComboboxContent>
    </InlineCombobox>
    );
    }
    export function HashtagElement(props: EditorElementProps<typeof HashtagPlugin>) {
    return (
    <EditorElement
    {...props}
    as="span"
    className="inline-block rounded-md bg-primary/10 px-1.5 py-0.5 align-baseline font-medium text-primary text-sm"
    >
    #{props.element.value}
    {props.children}
    </EditorElement>
    );
    }
    import * as React from 'react';
    import {
      type EditorElementProps,
      EditorElement,
    } from 'platejs/react';
    import {
      InlineCombobox,
      InlineComboboxContent,
      InlineComboboxEmpty,
      InlineComboboxItem,
    } from '@/components/editor/inline-combobox';
    import { HashtagPlugin } from './hashtag-plugin';
     
    const tags = ['Frontend', 'Backend', 'Design', 'Urgent'];
     
    export function HashtagCombobox({
      editableRef,
    }: {
      editableRef: React.RefObject<HTMLDivElement | null>;
    }) {
      return (
        <InlineCombobox editableRef={editableRef} plugin={HashtagPlugin}>
          <InlineComboboxContent>
            <InlineComboboxEmpty>No tags found</InlineComboboxEmpty>
            {tags.map((tag) => (
              <InlineComboboxItem
                key={tag}
                value={tag}
                onSelect={(tx) => {
                  tx.plugin(HashtagPlugin).insert({ value: tag });
                }}
              >
                #{tag}
              </InlineComboboxItem>
            ))}
          </InlineComboboxContent>
        </InlineCombobox>
      );
    }
     
    export function HashtagElement(props: EditorElementProps<typeof HashtagPlugin>) {
      return (
        <EditorElement
          {...props}
          as="span"
          className="inline-block rounded-md bg-primary/10 px-1.5 py-0.5 align-baseline font-medium text-primary text-sm"
        >
          #{props.element.value}
          {props.children}
        </EditorElement>
      );
    }
    { createEditor }
    from
    'platejs/react'
    ;
    import { HashtagPlugin } from './hashtag-plugin';
    import { HashtagCombobox, HashtagElement } from './hashtag-components';
    const editor = createEditor({
    plugins: [
    HashtagPlugin.configure({
    component: HashtagElement,
    slots: { afterEditable: HashtagCombobox },
    }),
    ],
    });
    'platejs/combobox/react'
    ;
    import { useEditor } from 'platejs/react';
    import * as React from 'react';
    import { cn } from '@/lib/utils';
    type FilterFn = (
    item: { value: string; group?: string; keywords?: string[]; label?: string },
    search: string
    ) => boolean;
    type InlineComboboxContextValue = {
    box: UseComboboxReturn;
    filter: FilterFn | false;
    match: ComboboxMatch | null;
    setHasEmpty: (hasEmpty: boolean) => void;
    };
    const InlineComboboxContext =
    React.createContext<InlineComboboxContextValue | null>(null);
    const useInlineComboboxContext = () => {
    const context = React.useContext(InlineComboboxContext);
    if (!context) {
    throw new Error('Inline combobox components require InlineCombobox');
    }
    return context;
    };
    /** The text typed after the trigger, or `''` when no trigger is active. */
    const useInlineComboboxQuery = () =>
    useInlineComboboxContext().match?.query ?? '';
    const defaultFilter: FilterFn = (
    { group, keywords = [], label, value },
    search
    ) => {
    const uniqueTerms = new Set(
    [value, ...keywords, group, label].flatMap((term) =>
    typeof term === 'string' ? [term] : []
    )
    );
    return Array.from(uniqueTerms).some((keyword) =>
    filterWords(keyword, search)
    );
    };
    const InlineCombobox = <P extends PluginReference>({
    children,
    editableRef,
    filter = defaultFilter,
    hideWhenNoValue = false,
    plugin,
    }: {
    children: React.ReactNode;
    editableRef: React.RefObject<HTMLDivElement | null>;
    plugin: UseComboboxOptions<P>['plugin'];
    filter?: FilterFn | false;
    hideWhenNoValue?: boolean;
    }) => {
    // Without a text input, a null active id would point at no option.
    const store = useComboboxStore({ includesBaseElement: false });
    const activeId = useStoreState(store, 'activeId');
    const items = useStoreState(store, 'items');
    const moves = useStoreState(store, 'moves');
    const renderedItems = useStoreState(store, 'renderedItems');
    const onKeyDown = React.useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
    if (store.getState().renderedItems.length === 0) return false;
    if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
    const down = event.key === 'ArrowDown';
    const next = down ? store.next() : store.previous();
    store.move(next ?? (down ? store.first() : store.last()));
    return true;
    }
    if (event.key !== 'Enter' && event.key !== 'Tab') return false;
    const { activeId: currentId, items: options } = store.getState();
    const option = (
    options.find((item) => item.id === currentId) ??
    options.find((item) => item.id === store.first())
    )?.element;
    if (!option) return false;
    option.click();
    return true;
    },
    [store]
    );
    const [shown, setShown] = React.useState(false);
    const box = useCombobox({
    activeOptionId: activeId ?? null,
    editableRef,
    onKeyDown,
    open: shown,
    plugin,
    });
    const { dismiss, match } = box;
    const query = match?.query ?? '';
    const [hasEmpty, setHasEmpty] = React.useState(false);
    const open =
    !!match &&
    (items.length > 0 || hasEmpty) &&
    (!hideWhenNoValue || query.length > 0);
    if (shown !== open) setShown(open);
    React.useEffect(() => {
    store.setAnchorElement(editableRef.current);
    }, [editableRef, store]);
    React.useEffect(() => {
    const { activeId: id } = store.getState();
    if (!id) {
    store.setActiveId(store.first());
    } else if (!renderedItems.some((item) => item.id === id)) {
    // A move scrolls the first option back into view; opening stays still.
    store.move(store.first());
    }
    }, [renderedItems, store]);
    // Ariakit scrolls on move only from its own Combobox input, which the
    // editor replaces. Scroll without focusing so the editor keeps focus.
    React.useEffect(() => {
    if (!moves) return;
    const { activeId: id } = store.getState();
    store.item(id)?.element?.scrollIntoView({
    block: 'nearest',
    inline: 'nearest',
    });
    }, [moves, store]);
    const queryBecameProse =
    !!match && !match.composing && items.length === 0 && /\s$/.test(query);
    React.useEffect(() => {
    if (queryBecameProse) dismiss();
    }, [dismiss, queryBecameProse]);
    const contextValue = React.useMemo<InlineComboboxContextValue>(
    () => ({ box, filter, match, setHasEmpty }),
    [box, filter, match]
    );
    return (
    <ComboboxProvider
    open={open}
    setOpen={(nextOpen) => {
    if (!nextOpen) dismiss();
    }}
    store={store}
    value={query}
    >
    <InlineComboboxContext value={contextValue}>
    {children}
    </InlineComboboxContext>
    </ComboboxProvider>
    );
    };
    const InlineComboboxContent = ({
    className,
    ...props
    }: React.HTMLAttributes<HTMLDivElement>) => {
    const editor = useEditor();
    const { box, match } = useInlineComboboxContext();
    const getAnchorRect = React.useCallback(() => {
    if (!match) return null;
    const start = match.range.anchor;
    return editor.api.dom.resolveRangeRect({ anchor: start, focus: start });
    }, [editor, match]);
    return (
    <Portal>
    <ComboboxPopover
    id={box.listboxId}
    className={cn(
    'cn-command cn-command-list z-500 w-[300px] overflow-x-hidden overflow-y-auto shadow-md',
    className
    )}
    autoFocusOnHide={false}
    autoFocusOnShow={false}
    getAnchorRect={getAnchorRect}
    hideOnEscape={false}
    hideOnInteractOutside={(event) =>
    !(event.target instanceof Node) ||
    !editor.api.dom.root()?.contains(event.target)
    }
    modal={false}
    // Keep the editor focused unless an IME is composing: the blur ends it.
    onMouseDown={(event) => {
    if (!match?.composing) event.preventDefault();
    }}
    {...props}
    />
    </Portal>
    );
    };
    const comboboxItemVariants = cva(
    'cn-command-item mx-1 h-7 cursor-pointer text-foreground transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground'
    );
    const InlineComboboxItem = ({
    className,
    focusEditor = true,
    group,
    keywords,
    label,
    onClick,
    onSelect,
    ...props
    }: Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect' | 'value'> & {
    focusEditor?: boolean;
    group?: string;
    keywords?: string[];
    label?: string;
    value: string;
    /** Insert the completion. Return false to refuse and keep the query. */
    onSelect?: (tx: PluginTransaction) => false | void;
    }) => {
    const { value } = props;
    const editor = useEditor();
    const { box, filter, match } = useInlineComboboxContext();
    const search = match?.query ?? '';
    const visible = React.useMemo(
    () => !filter || filter({ group, keywords, label, value }, search),
    [filter, group, keywords, label, value, search]
    );
    if (!visible || !match) return null;
    return (
    <ComboboxItem
    className={cn(comboboxItemVariants(), className)}
    hideOnClick={false}
    setValueOnClick={false}
    onClick={(event) => {
    if (!box.complete(match, (tx) => onSelect?.(tx))) return;
    if (focusEditor) editor.api.dom.focus();
    onClick?.(event);
    }}
    {...props}
    />
    );
    };
    const InlineComboboxEmpty = ({
    children,
    className,
    }: React.HTMLAttributes<HTMLDivElement>) => {
    const { setHasEmpty } = useInlineComboboxContext();
    const store = useComboboxContext();
    if (store == null) {
    throw new Error('InlineComboboxEmpty requires a Combobox store');
    }
    const items = useStoreState(store, 'items');
    React.useEffect(() => {
    setHasEmpty(true);
    return () => {
    setHasEmpty(false);
    };
    }, [setHasEmpty]);
    if (items.length > 0) return null;
    return <div className={cn('cn-command-empty', className)}>{children}</div>;
    };
    function InlineComboboxGroup({
    className,
    ...props
    }: React.HTMLAttributes<HTMLDivElement>) {
    return (
    <ComboboxGroup
    {...props}
    className={cn(
    'cn-command-group hidden not-last:border-b [&:has([role=option])]:block',
    className
    )}
    />
    );
    }
    function InlineComboboxGroupLabel({
    className,
    ...props
    }: React.HTMLAttributes<HTMLDivElement>) {
    return (
    <ComboboxGroupLabel
    {...props}
    className={cn(
    'mt-1.5 mb-2 px-3 font-medium text-muted-foreground text-xs',
    className
    )}
    />
    );
    }
    export {
    InlineCombobox,
    InlineComboboxContent,
    InlineComboboxEmpty,
    InlineComboboxGroup,
    InlineComboboxGroupLabel,
    InlineComboboxItem,
    useInlineComboboxQuery,
    };