{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inline-combobox",
  "title": "Inline Combobox",
  "description": "A combobox for inline suggestions.",
  "dependencies": [
    "@platejs/combobox",
    "@ariakit/react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/registry/ui/inline-combobox.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport type { PointRef, TElement } from 'platejs';\n\nimport {\n  type ComboboxItemProps,\n  Combobox,\n  ComboboxGroup,\n  ComboboxGroupLabel,\n  ComboboxItem,\n  ComboboxPopover,\n  ComboboxProvider,\n  ComboboxRow,\n  Portal,\n  useComboboxContext,\n  useComboboxStore,\n} from '@ariakit/react';\nimport { filterWords } from '@platejs/combobox';\nimport {\n  type UseComboboxInputResult,\n  useComboboxInput,\n  useHTMLInputCursorState,\n} from '@platejs/combobox/react';\nimport { cva } from 'class-variance-authority';\nimport { useComposedRef, useEditorRef } from 'platejs/react';\n\nimport { cn } from '@/lib/utils';\n\ntype FilterFn = (\n  item: { value: string; group?: string; keywords?: string[]; label?: string },\n  search: string\n) => boolean;\n\ntype InlineComboboxContextValue = {\n  filter: FilterFn | false;\n  inputProps: UseComboboxInputResult['props'];\n  inputRef: React.RefObject<HTMLInputElement | null>;\n  removeInput: UseComboboxInputResult['removeInput'];\n  showTrigger: boolean;\n  trigger: string;\n  setHasEmpty: (hasEmpty: boolean) => void;\n};\n\nconst InlineComboboxContext = React.createContext<InlineComboboxContextValue>(\n  null as unknown as InlineComboboxContextValue\n);\n\nconst defaultFilter: FilterFn = (\n  { group, keywords = [], label, value },\n  search\n) => {\n  const uniqueTerms = new Set(\n    [value, ...keywords, group, label].filter(Boolean)\n  );\n\n  return Array.from(uniqueTerms).some((keyword) =>\n    filterWords(keyword!, search)\n  );\n};\n\ntype InlineComboboxProps = {\n  children: React.ReactNode;\n  element: TElement;\n  trigger: string;\n  filter?: FilterFn | false;\n  hideWhenNoValue?: boolean;\n  showTrigger?: boolean;\n  value?: string;\n  setValue?: (value: string) => void;\n};\n\nconst InlineCombobox = ({\n  children,\n  element,\n  filter = defaultFilter,\n  hideWhenNoValue = false,\n  setValue: setValueProp,\n  showTrigger = true,\n  trigger,\n  value: valueProp,\n}: InlineComboboxProps) => {\n  const editor = useEditorRef();\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const cursorState = useHTMLInputCursorState(inputRef);\n\n  const [valueState, setValueState] = React.useState('');\n  const hasValueProp = valueProp !== undefined;\n  const value = hasValueProp ? valueProp : valueState;\n\n  // Check if current user is the creator of this element (for Yjs collaboration)\n  const isCreator = React.useMemo(() => {\n    const elementUserId = (element as any).userId;\n    const currentUserId = editor.meta.userId;\n\n    // If no userId (backwards compatibility or non-Yjs), allow\n    if (!elementUserId) return true;\n\n    return elementUserId === currentUserId;\n  }, [editor.meta.userId, element]);\n\n  const setValue = React.useCallback(\n    (newValue: string) => {\n      setValueProp?.(newValue);\n\n      if (!hasValueProp) {\n        setValueState(newValue);\n      }\n    },\n    [setValueProp, hasValueProp]\n  );\n\n  /**\n   * Track the point just before the input element so we know where to\n   * insertText if the combobox closes due to a selection change.\n   */\n  const insertPointRef = React.useRef<PointRef | null>(null);\n\n  React.useEffect(() => {\n    insertPointRef.current?.unref();\n    insertPointRef.current = null;\n\n    const path = editor.api.findPath(element);\n\n    if (!path) return;\n\n    const point = editor.api.before(path);\n\n    if (!point) return;\n\n    const pointRef = editor.api.pointRef(point);\n    insertPointRef.current = pointRef;\n\n    return () => {\n      if (insertPointRef.current === pointRef) {\n        insertPointRef.current = null;\n      }\n      pointRef.unref();\n    };\n  }, [editor, element]);\n\n  const { props: inputProps, removeInput } = useComboboxInput({\n    cancelInputOnBlur: true,\n    cursorState,\n    autoFocus: isCreator,\n    ref: inputRef,\n    onCancelInput: (cause) => {\n      if (cause !== 'backspace') {\n        editor.tf.insertText(trigger + value, {\n          at: insertPointRef.current?.current ?? undefined,\n        });\n      }\n      if (cause === 'arrowLeft' || cause === 'arrowRight') {\n        editor.tf.move({\n          distance: 1,\n          reverse: cause === 'arrowLeft',\n        });\n      }\n    },\n  });\n\n  const [hasEmpty, setHasEmpty] = React.useState(false);\n\n  const contextValue: InlineComboboxContextValue = React.useMemo(\n    () => ({\n      filter,\n      inputProps,\n      inputRef,\n      removeInput,\n      setHasEmpty,\n      showTrigger,\n      trigger,\n    }),\n    [\n      trigger,\n      showTrigger,\n      filter,\n      inputRef,\n      inputProps,\n      removeInput,\n      setHasEmpty,\n    ]\n  );\n\n  const store = useComboboxStore({\n    // open: ,\n    setValue: (newValue) => React.startTransition(() => setValue(newValue)),\n  });\n\n  const items = store.useState('items');\n\n  /**\n   * If there is no active ID and the list of items changes, select the first\n   * item.\n   */\n  React.useEffect(() => {\n    if (!store.getState().activeId) {\n      store.setActiveId(store.first());\n    }\n  }, [items, store]);\n\n  return (\n    <span contentEditable={false}>\n      <ComboboxProvider\n        open={\n          (items.length > 0 || hasEmpty) &&\n          (!hideWhenNoValue || value.length > 0)\n        }\n        store={store}\n      >\n        <InlineComboboxContext.Provider value={contextValue}>\n          {children}\n        </InlineComboboxContext.Provider>\n      </ComboboxProvider>\n    </span>\n  );\n};\n\nconst InlineComboboxInput = ({\n  className,\n  ref: propRef,\n  ...props\n}: React.HTMLAttributes<HTMLInputElement> & {\n  ref?: React.RefObject<HTMLInputElement | null>;\n}) => {\n  const {\n    inputProps,\n    inputRef: contextRef,\n    showTrigger,\n    trigger,\n  } = React.useContext(InlineComboboxContext);\n\n  const store = useComboboxContext()!;\n  const value = store.useState('value');\n\n  const ref = useComposedRef(propRef, contextRef);\n\n  /**\n   * To create an auto-resizing input, we render a visually hidden span\n   * containing the input value and position the input element on top of it.\n   * This works well for all cases except when input exceeds the width of the\n   * container.\n   */\n\n  return (\n    <>\n      {showTrigger && trigger}\n\n      <span className=\"relative min-h-[1lh]\">\n        <span\n          className=\"invisible overflow-hidden text-nowrap\"\n          aria-hidden=\"true\"\n        >\n          {value || '\\u200B'}\n        </span>\n\n        <Combobox\n          ref={ref}\n          className={cn(\n            'absolute top-0 left-0 size-full bg-transparent outline-none',\n            className\n          )}\n          value={value}\n          autoSelect\n          {...inputProps}\n          {...props}\n        />\n      </span>\n    </>\n  );\n};\n\nInlineComboboxInput.displayName = 'InlineComboboxInput';\n\nconst InlineComboboxContent: typeof ComboboxPopover = ({\n  className,\n  ...props\n}) => {\n  // Portal prevents CSS from leaking into popover\n  const store = useComboboxContext();\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    if (!store) return;\n\n    const state = store.getState();\n    const { items, activeId } = state;\n\n    if (!items.length) return;\n\n    const currentIndex = items.findIndex((item) => item.id === activeId);\n\n    if (event.key === 'ArrowUp' && currentIndex <= 0) {\n      event.preventDefault();\n      store.setActiveId(store.last());\n    } else if (event.key === 'ArrowDown' && currentIndex >= items.length - 1) {\n      event.preventDefault();\n      store.setActiveId(store.first());\n    }\n  }\n\n  return (\n    <Portal>\n      <ComboboxPopover\n        className={cn(\n          'z-500 max-h-[288px] w-[300px] overflow-y-auto rounded-md bg-popover shadow-md',\n          className\n        )}\n        onKeyDownCapture={handleKeyDown}\n        {...props}\n      />\n    </Portal>\n  );\n};\n\nconst comboboxItemVariants = cva(\n  'relative mx-1 flex h-[28px] select-none items-center rounded-sm px-2 text-foreground text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n  {\n    defaultVariants: {\n      interactive: true,\n    },\n    variants: {\n      interactive: {\n        false: '',\n        true: 'cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground',\n      },\n    },\n  }\n);\n\nconst InlineComboboxItem = ({\n  className,\n  focusEditor = true,\n  group,\n  keywords,\n  label,\n  onClick,\n  ...props\n}: {\n  focusEditor?: boolean;\n  group?: string;\n  keywords?: string[];\n  label?: string;\n} & ComboboxItemProps &\n  Required<Pick<ComboboxItemProps, 'value'>>) => {\n  const { value } = props;\n\n  const { filter, removeInput } = React.useContext(InlineComboboxContext);\n\n  const store = useComboboxContext()!;\n\n  // Optimization: Do not subscribe to value if filter is false\n  const search = filter && store.useState('value');\n\n  const visible = React.useMemo(\n    () =>\n      !filter || filter({ group, keywords, label, value }, search as string),\n    [filter, group, keywords, label, value, search]\n  );\n\n  if (!visible) return null;\n\n  return (\n    <ComboboxItem\n      className={cn(comboboxItemVariants(), className)}\n      onClick={(event) => {\n        removeInput(focusEditor);\n        onClick?.(event);\n      }}\n      {...props}\n    />\n  );\n};\n\nconst InlineComboboxEmpty = ({\n  children,\n  className,\n}: React.HTMLAttributes<HTMLDivElement>) => {\n  const { setHasEmpty } = React.useContext(InlineComboboxContext);\n  const store = useComboboxContext()!;\n  const items = store.useState('items');\n\n  React.useEffect(() => {\n    setHasEmpty(true);\n\n    return () => {\n      setHasEmpty(false);\n    };\n  }, [setHasEmpty]);\n\n  if (items.length > 0) return null;\n\n  return (\n    <div\n      className={cn(comboboxItemVariants({ interactive: false }), className)}\n    >\n      {children}\n    </div>\n  );\n};\n\nconst InlineComboboxRow = ComboboxRow;\n\nfunction InlineComboboxGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof ComboboxGroup>) {\n  return (\n    <ComboboxGroup\n      {...props}\n      className={cn(\n        'hidden not-last:border-b py-1.5 [&:has([role=option])]:block',\n        className\n      )}\n    />\n  );\n}\n\nfunction InlineComboboxGroupLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof ComboboxGroupLabel>) {\n  return (\n    <ComboboxGroupLabel\n      {...props}\n      className={cn(\n        'mt-1.5 mb-2 px-3 font-medium text-muted-foreground text-xs',\n        className\n      )}\n    />\n  );\n}\n\nexport {\n  InlineCombobox,\n  InlineComboboxContent,\n  InlineComboboxEmpty,\n  InlineComboboxGroup,\n  InlineComboboxGroupLabel,\n  InlineComboboxInput,\n  InlineComboboxItem,\n  InlineComboboxRow,\n};\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "docs": [
      {
        "route": "/docs/combobox"
      },
      {
        "route": "https://pro.platejs.org/docs/components/inline-combobox"
      }
    ],
    "examples": [
      "mention-demo",
      "slash-command-demo",
      "emoji-demo"
    ]
  },
  "type": "registry:ui"
}