{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "chat-input",
	"type": "registry:ui",
	"title": "Chat Input",
	"description": "A chat input component with mention support and automatic height adjustment.",
	"dependencies": [
		"@tiptap/core",
		"@tiptap/react",
		"@tiptap/starter-kit",
		"@tiptap/extension-mention",
		"@tiptap/extension-placeholder",
		"@tiptap/suggestion",
		"tippy.js"
	],
	"registryDependencies": ["input-group", "button"],
	"files": [
		{
			"path": "./src/registry/ui/chat-input.tsx",
			"content": "\"use client\";\n\nimport { Extension } from \"@tiptap/core\";\nimport { Mention as MentionExtension } from \"@tiptap/extension-mention\";\nimport Placeholder from \"@tiptap/extension-placeholder\";\nimport type { Editor, JSONContent } from \"@tiptap/react\";\nimport { EditorContent, ReactRenderer, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport type { SuggestionProps } from \"@tiptap/suggestion\";\nimport { ArrowUpIcon, Loader2, PlusIcon } from \"lucide-react\";\n\nimport {\n\ttype ComponentProps,\n\tcreateContext,\n\tforwardRef,\n\ttype ReactNode,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseImperativeHandle,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport tippy, { type Instance } from \"tippy.js\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n\tInputGroup,\n\tInputGroupAddon,\n\tInputGroupButton,\n\tInputGroupText,\n} from \"@/components/ui/input-group\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ChatInputValue = JSONContent;\n\nexport type BaseMentionItem = {\n\tid: string;\n\tname: string;\n};\n\ntype MentionConfig<T extends BaseMentionItem = BaseMentionItem> = {\n\ttype: string;\n\ttrigger: string; // e.g., '@' or '/'\n\titems: T[];\n\trenderItem?: (item: T, isSelected: boolean) => ReactNode;\n\teditorMentionClass?: string;\n};\n\nexport function createMentionConfig<\n\tT extends BaseMentionItem = BaseMentionItem,\n>(config: MentionConfig<T>): MentionConfig<T> {\n\treturn config;\n}\n\ntype ChatInputContextType = {\n\t// biome-ignore lint/suspicious/noExplicitAny: Needs to accept configs with different item types\n\tmentionConfigs: MentionConfig<any>[];\n\t// biome-ignore lint/suspicious/noExplicitAny: Needs to accept configs with different item types\n\taddMentionConfig: (config: MentionConfig<any>) => void;\n\tonSubmit: () => void;\n\tonStop?: () => void;\n\tisStreaming: boolean;\n\tdisabled: boolean;\n\tvalue?: ChatInputValue;\n\tonChange?: (value: ChatInputValue) => void;\n\teditor: Editor | null;\n\tsetEditor: (editor: Editor | null) => void;\n};\n\nconst ChatInputContext = createContext<ChatInputContextType>({\n\tmentionConfigs: [],\n\taddMentionConfig: () => {},\n\tonSubmit: () => {},\n\tonStop: undefined,\n\tisStreaming: false,\n\tdisabled: false,\n\tvalue: undefined,\n\tonChange: undefined,\n\teditor: null,\n\tsetEditor: () => {},\n});\n\nexport function ChatInput({\n\tchildren,\n\tclassName,\n\tonSubmit,\n\tisStreaming = false,\n\tonStop,\n\tdisabled = false,\n\tvalue,\n\tonChange,\n\t...props\n}: ComponentProps<typeof InputGroup> & {\n\tonSubmit: () => void;\n\tisStreaming?: boolean;\n\tonStop?: () => void;\n\tdisabled?: boolean;\n\tvalue?: ChatInputValue;\n\tonChange?: (value: ChatInputValue) => void;\n}) {\n\t// biome-ignore lint/suspicious/noExplicitAny: Needs to accept configs with different item types\n\tconst [mentionConfigs, setMentionConfigs] = useState<MentionConfig<any>[]>(\n\t\t[],\n\t);\n\tconst [editor, setEditor] = useState<Editor | null>(null);\n\n\tconst registeredTypesRef = useRef(new Set<string>());\n\n\t// biome-ignore lint/suspicious/noExplicitAny: Needs to accept configs with different item types\n\tconst addMentionConfig = useCallback((config: MentionConfig<any>) => {\n\t\tif (registeredTypesRef.current.has(config.type)) {\n\t\t\tsetMentionConfigs((prev) => {\n\t\t\t\tconst existingIndex = prev.findIndex(\n\t\t\t\t\t(c) => c.type === config.type,\n\t\t\t\t);\n\t\t\t\tif (existingIndex >= 0) {\n\t\t\t\t\tconst updated = [...prev];\n\t\t\t\t\tupdated[existingIndex] = config;\n\t\t\t\t\treturn updated;\n\t\t\t\t}\n\t\t\t\treturn prev;\n\t\t\t});\n\t\t} else {\n\t\t\tregisteredTypesRef.current.add(config.type);\n\t\t\tsetMentionConfigs((prev) => [...prev, config]);\n\t\t}\n\t}, []);\n\n\treturn (\n\t\t<ChatInputContext.Provider\n\t\t\tvalue={{\n\t\t\t\tmentionConfigs,\n\t\t\t\taddMentionConfig,\n\t\t\t\tonSubmit,\n\t\t\t\tonStop,\n\t\t\t\tisStreaming,\n\t\t\t\tdisabled,\n\t\t\t\tvalue,\n\t\t\t\tonChange,\n\t\t\t\teditor,\n\t\t\t\tsetEditor,\n\t\t\t}}\n\t\t>\n\t\t\t<InputGroup\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"focus-within:ring-1 focus-within:ring-ring rounded-2xl\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</InputGroup>\n\t\t</ChatInputContext.Provider>\n\t);\n}\n\nexport interface ChatInputEditorProps {\n\tdisabled?: boolean;\n\tonEnter?: () => void;\n\tplaceholder?: string;\n\tclassName?: string;\n\tvalue?: ChatInputValue;\n\tonChange?: (value: ChatInputValue) => void;\n}\n\nexport function ChatInputEditor({\n\tdisabled,\n\tonEnter,\n\tplaceholder = \"Type a message...\",\n\tclassName,\n\tvalue,\n\tonChange,\n}: ChatInputEditorProps) {\n\tconst {\n\t\tmentionConfigs,\n\t\tonSubmit,\n\t\tdisabled: contextDisabled,\n\t\tvalue: contextValue,\n\t\tonChange: contextOnChange,\n\t\tsetEditor,\n\t} = useContext(ChatInputContext);\n\n\tconst effectiveValue = value ?? contextValue;\n\tconst effectiveOnChange = onChange ?? contextOnChange;\n\tconst [isMounted, setIsMounted] = useState(false);\n\n\tuseEffect(() => {\n\t\tsetIsMounted(true);\n\t}, []);\n\n\tconst onEnterRef = useRef(onEnter || onSubmit);\n\n\tuseEffect(() => {\n\t\tonEnterRef.current = onEnter || onSubmit;\n\t}, [onEnter, onSubmit]);\n\n\tconst extensions = useMemo(\n\t\t() => [\n\t\t\tStarterKit,\n\t\t\tPlaceholder.configure({ placeholder }),\n\t\t\tKeyboardShortcuts.configure({\n\t\t\t\tgetOnEnter: () => onEnterRef.current,\n\t\t\t}),\n\t\t\t...mentionConfigs.map((config) => {\n\t\t\t\tconst MentionPlugin = MentionExtension.extend({\n\t\t\t\t\tname: `${config.type}-mention`,\n\t\t\t\t});\n\t\t\t\treturn MentionPlugin.configure({\n\t\t\t\t\tHTMLAttributes: {\n\t\t\t\t\t\tclass: cn(\n\t\t\t\t\t\t\t\"bg-primary text-primary-foreground rounded-sm px-1 py-0.5 no-underline\",\n\t\t\t\t\t\t\tconfig.editorMentionClass,\n\t\t\t\t\t\t),\n\t\t\t\t\t},\n\t\t\t\t\tsuggestion: {\n\t\t\t\t\t\tchar: config.trigger,\n\t\t\t\t\t\t...getMentionSuggestion(config),\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}),\n\t\t],\n\t\t[mentionConfigs, placeholder],\n\t);\n\n\tconst onUpdate = useCallback(\n\t\t({ editor }: { editor: Editor }) => {\n\t\t\tif (isMounted) {\n\t\t\t\teffectiveOnChange?.(editor.getJSON());\n\t\t\t}\n\t\t},\n\t\t[effectiveOnChange, isMounted],\n\t);\n\n\tconst editor = useEditor(\n\t\t{\n\t\t\textensions,\n\t\t\tcontent: effectiveValue,\n\t\t\tonUpdate,\n\t\t\teditable: !(disabled || contextDisabled),\n\t\t\timmediatelyRender: false,\n\t\t},\n\t\t[extensions, disabled, contextDisabled],\n\t);\n\n\tuseEffect(() => {\n\t\tif (editor) {\n\t\t\tsetEditor(editor);\n\t\t}\n\t\treturn () => setEditor(null);\n\t}, [editor, setEditor]);\n\n\tuseEffect(() => {\n\t\tif (\n\t\t\teffectiveValue &&\n\t\t\teditor &&\n\t\t\tJSON.stringify(effectiveValue) !== JSON.stringify(editor.getJSON())\n\t\t) {\n\t\t\teditor.commands.setContent(effectiveValue);\n\t\t}\n\t}, [effectiveValue, editor]);\n\n\treturn (\n\t\t<>\n\t\t\t<style>{`\n\t\t\t\t.tiptap:focus { outline: none; }\n\t\t\t\t.tiptap p.is-editor-empty:first-child::before {\n\t\t\t\t\tcolor: var(--muted-foreground);\n\t\t\t\t\tcontent: attr(data-placeholder);\n\t\t\t\t\tfloat: left;\n\t\t\t\t\theight: 0;\n\t\t\t\t\tpointer-events: none;\n\t\t\t\t}\n\t\t\t`}</style>\n\t\t\t<EditorContent\n\t\t\t\teditor={editor}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"w-full h-full max-h-48 px-4 pt-4 pb-2 overflow-y-auto\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t/>\n\t\t</>\n\t);\n}\n\nconst KeyboardShortcuts = Extension.create({\n\taddKeyboardShortcuts() {\n\t\treturn {\n\t\t\tEnter: () => {\n\t\t\t\tconst onEnter = this.options.getOnEnter?.();\n\t\t\t\tif (onEnter) {\n\t\t\t\t\tonEnter();\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\t},\n\taddOptions() {\n\t\treturn {\n\t\t\tgetOnEnter: () => () => {},\n\t\t};\n\t},\n});\n\nexport type ChatInputMentionProps<T extends BaseMentionItem = BaseMentionItem> =\n\t{\n\t\ttype: string;\n\t\ttrigger: string;\n\t\titems: T[];\n\t\tchildren?: (item: T, isSelected: boolean) => ReactNode;\n\t\teditorMentionClass?: string;\n\t};\n\nexport function ChatInputMention<T extends BaseMentionItem = BaseMentionItem>({\n\ttype,\n\ttrigger,\n\titems,\n\tchildren,\n\teditorMentionClass,\n}: ChatInputMentionProps<T>) {\n\tconst { addMentionConfig } = useContext(ChatInputContext);\n\n\tconst renderItemRef = useRef(children);\n\tuseEffect(() => {\n\t\trenderItemRef.current = children;\n\t}, [children]);\n\n\tuseEffect(() => {\n\t\taddMentionConfig({\n\t\t\ttype,\n\t\t\ttrigger,\n\t\t\titems,\n\t\t\trenderItem: renderItemRef.current,\n\t\t\teditorMentionClass,\n\t\t});\n\t}, [addMentionConfig, type, trigger, items, editorMentionClass]);\n\n\treturn null;\n}\n\ninterface GenericMentionListProps<T extends BaseMentionItem> {\n\titems: T[];\n\tcommand: (item: { id: string; label: string }) => void;\n\trenderItem?: (item: T, isSelected: boolean) => ReactNode;\n}\n\ntype GenericMentionListRef = {\n\thandleKeyDown: (event: KeyboardEvent) => boolean;\n};\n\nconst GenericMentionList = forwardRef(\n\t<T extends BaseMentionItem>(\n\t\tprops: GenericMentionListProps<T>,\n\t\tref: React.Ref<GenericMentionListRef>,\n\t) => {\n\t\tconst { items, command, renderItem } = props;\n\t\tconst [selectedIndex, setSelectedIndex] = useState(0);\n\t\tconst itemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n\t\tconst selectItem = useCallback(\n\t\t\t(index: number) => {\n\t\t\t\tconst item = items[index];\n\t\t\t\tif (item) {\n\t\t\t\t\tcommand({\n\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\tlabel: item.name,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t\t[items, command],\n\t\t);\n\n\t\tconst scrollToItem = useCallback((index: number) => {\n\t\t\tconst itemEl = itemRefs.current[index];\n\t\t\tif (itemEl) {\n\t\t\t\titemEl.scrollIntoView({\n\t\t\t\t\tbehavior: \"smooth\",\n\t\t\t\t\tblock: \"nearest\",\n\t\t\t\t});\n\t\t\t}\n\t\t}, []);\n\n\t\tconst upHandler = useCallback(() => {\n\t\t\tsetSelectedIndex((prevIndex) => {\n\t\t\t\tconst newIndex = (prevIndex + items.length - 1) % items.length;\n\t\t\t\tscrollToItem(newIndex);\n\t\t\t\treturn newIndex;\n\t\t\t});\n\t\t}, [items.length, scrollToItem]);\n\n\t\tconst downHandler = useCallback(() => {\n\t\t\tsetSelectedIndex((prevIndex) => {\n\t\t\t\tconst newIndex = (prevIndex + 1) % items.length;\n\t\t\t\tscrollToItem(newIndex);\n\t\t\t\treturn newIndex;\n\t\t\t});\n\t\t}, [items.length, scrollToItem]);\n\n\t\tconst enterHandler = useCallback(() => {\n\t\t\tselectItem(selectedIndex);\n\t\t}, [selectedIndex, selectItem]);\n\n\t\tuseEffect(() => {\n\t\t\tsetSelectedIndex(0);\n\t\t\titemRefs.current = itemRefs.current.slice(0, items.length);\n\t\t}, [items]);\n\n\t\tconst handleKeyDown = useCallback(\n\t\t\t(event: KeyboardEvent) => {\n\t\t\t\tif (event.key === \"ArrowUp\") {\n\t\t\t\t\tupHandler();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (event.key === \"ArrowDown\") {\n\t\t\t\t\tdownHandler();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (event.key === \"Enter\") {\n\t\t\t\t\tenterHandler();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t},\n\t\t\t[upHandler, downHandler, enterHandler],\n\t\t);\n\n\t\tuseImperativeHandle(ref, () => ({ handleKeyDown }), [handleKeyDown]);\n\n\t\treturn (\n\t\t\t<div className=\"min-w-48 max-w-64 max-h-48 bg-popover text-popover-foreground border border-border rounded-lg shadow-md flex flex-col gap-1 overflow-y-auto p-1\">\n\t\t\t\t{items.length ? (\n\t\t\t\t\titems.map((item, index) => (\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"flex justify-start px-1 py-2 gap-2\",\n\t\t\t\t\t\t\t\tselectedIndex === index && \"bg-accent\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\tonClick={() => selectItem(index)}\n\t\t\t\t\t\t\tref={(el) => {\n\t\t\t\t\t\t\t\tif (el) {\n\t\t\t\t\t\t\t\t\titemRefs.current[index] = el;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{renderItem ? (\n\t\t\t\t\t\t\t\trenderItem(item, selectedIndex === index)\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<span className=\"px-2\">{item.name}</span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t))\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"text-sm text-muted-foreground px-2 py-1.5\">\n\t\t\t\t\t\tNo results found\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t},\n);\n\nfunction getMentionSuggestion<T extends BaseMentionItem>(\n\tconfig: MentionConfig<T>,\n) {\n\treturn {\n\t\titems: ({ query }: { query: string }) => {\n\t\t\treturn config.items.filter((item) =>\n\t\t\t\titem.name.toLowerCase().startsWith(query.toLowerCase()),\n\t\t\t);\n\t\t},\n\t\trender: () => {\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: Ok\n\t\t\tlet component: ReactRenderer<any>;\n\t\t\tlet popup: Instance;\n\n\t\t\treturn {\n\t\t\t\tonStart: (props: SuggestionProps<T>) => {\n\t\t\t\t\tcomponent = new ReactRenderer(GenericMentionList, {\n\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\titems: props.items,\n\t\t\t\t\t\t\tcommand: props.command,\n\t\t\t\t\t\t\trenderItem: config.renderItem,\n\t\t\t\t\t\t},\n\t\t\t\t\t\teditor: props.editor,\n\t\t\t\t\t});\n\n\t\t\t\t\tif (!props.clientRect) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tpopup = tippy(document.body, {\n\t\t\t\t\t\tgetReferenceClientRect:\n\t\t\t\t\t\t\tprops.clientRect as () => DOMRect,\n\t\t\t\t\t\tappendTo: () => document.body,\n\t\t\t\t\t\tcontent: component.element,\n\t\t\t\t\t\tshowOnCreate: true,\n\t\t\t\t\t\tinteractive: true,\n\t\t\t\t\t\ttrigger: \"manual\",\n\t\t\t\t\t\tplacement: \"bottom-start\",\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tonUpdate: (props: SuggestionProps<T>) => {\n\t\t\t\t\tcomponent.updateProps(props);\n\n\t\t\t\t\tif (!props.clientRect) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tpopup.setProps({\n\t\t\t\t\t\tgetReferenceClientRect:\n\t\t\t\t\t\t\tprops.clientRect as () => DOMRect,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tonKeyDown: (props: { event: KeyboardEvent }) => {\n\t\t\t\t\tif (props.event.key === \"Escape\") {\n\t\t\t\t\t\tpopup.hide();\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\treturn component.ref?.handleKeyDown?.(props.event) || false;\n\t\t\t\t},\n\t\t\t\tonExit: () => {\n\t\t\t\t\tpopup.destroy();\n\t\t\t\t\tcomponent.destroy();\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport type ChatInputSubmitButtonProps = ComponentProps<\n\ttypeof InputGroupButton\n> & {\n\tisStreaming?: boolean;\n\tonStop?: () => void;\n\tdisabled?: boolean;\n};\n\nexport function ChatInputSubmitButton({\n\tclassName,\n\tisStreaming,\n\tonStop,\n\tdisabled,\n\t...props\n}: ChatInputSubmitButtonProps) {\n\tconst {\n\t\tonSubmit,\n\t\tonStop: onStopContext,\n\t\tisStreaming: isStreamingContext,\n\t\tdisabled: contextDisabled,\n\t} = useContext(ChatInputContext);\n\n\tconst loading = isStreaming ?? isStreamingContext;\n\tconst effectiveOnStop = onStop ?? onStopContext;\n\tconst effectiveDisabled = disabled ?? contextDisabled;\n\n\tconst isStopVariant = loading && effectiveOnStop;\n\tconst isLoadingVariant = loading && !effectiveOnStop;\n\n\tconst handleClick = isStopVariant ? effectiveOnStop : onSubmit;\n\n\tif (isStopVariant) {\n\t\treturn (\n\t\t\t<InputGroupButton\n\t\t\t\tvariant=\"default\"\n\t\t\t\tsize=\"icon-sm\"\n\t\t\t\tclassName={cn(\"rounded-full\", className)}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t<StopIcon className=\"h-4 w-4\" />\n\n\t\t\t\t<span className=\"sr-only\">Stop</span>\n\t\t\t</InputGroupButton>\n\t\t);\n\t}\n\n\tif (isLoadingVariant) {\n\t\treturn (\n\t\t\t<InputGroupButton\n\t\t\t\tvariant=\"default\"\n\t\t\t\tsize=\"icon-sm\"\n\t\t\t\tclassName={cn(\"rounded-full\", className)}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t<Loader2 className=\"h-4 w-4 animate-spin\" />\n\t\t\t\t<span className=\"sr-only\">Loading</span>\n\t\t\t</InputGroupButton>\n\t\t);\n\t}\n\n\treturn (\n\t\t<InputGroupButton\n\t\t\tvariant=\"default\"\n\t\t\tsize=\"icon-sm\"\n\t\t\tclassName={cn(\"rounded-full\", className)}\n\t\t\tonClick={handleClick}\n\t\t\tdisabled={effectiveDisabled}\n\t\t\t{...props}\n\t\t>\n\t\t\t<ArrowUpIcon />\n\t\t\t<span className=\"sr-only\">Send</span>\n\t\t</InputGroupButton>\n\t);\n}\n\nconst StopIcon = ({ className }: { className?: string }) => (\n\t<svg\n\t\twidth=\"16\"\n\t\theight=\"16\"\n\t\tviewBox=\"0 0 16 16\"\n\t\tfill=\"currentColor\"\n\t\tclassName={className}\n\t\taria-hidden=\"true\"\n\t>\n\t\t<title>Stop</title>\n\t\t<rect x=\"2\" y=\"2\" width=\"12\" height=\"12\" rx=\"2\" fill=\"currentColor\" />\n\t</svg>\n);\n\nexport function ChatInputMentionButton({\n\tclassName,\n\t...props\n}: ComponentProps<typeof InputGroupButton>) {\n\tconst { mentionConfigs, editor } = useContext(ChatInputContext);\n\n\tif (!mentionConfigs.length) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<DropdownMenu>\n\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t<InputGroupButton\n\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\tclassName={cn(\"rounded-full shrink-0\", className)}\n\t\t\t\t\t{...props}\n\t\t\t\t>\n\t\t\t\t\t<PlusIcon className=\"h-4 w-4\" />\n\t\t\t\t\t<span className=\"sr-only\">Add Mention</span>\n\t\t\t\t</InputGroupButton>\n\t\t\t</DropdownMenuTrigger>\n\t\t\t<DropdownMenuContent align=\"start\" className=\"w-48\">\n\t\t\t\t{mentionConfigs.map((config) => (\n\t\t\t\t\t<DropdownMenuItem\n\t\t\t\t\t\tkey={config.type}\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tif (editor) {\n\t\t\t\t\t\t\t\teditor.commands.insertContent(config.trigger);\n\t\t\t\t\t\t\t\teditor.commands.focus();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{config.trigger} {config.type}\n\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t))}\n\t\t\t</DropdownMenuContent>\n\t\t</DropdownMenu>\n\t);\n}\n\nexport type ChatInputGroupAddon = ComponentProps<typeof InputGroupAddon>;\n\nexport function ChatInputGroupAddon({\n\tclassName,\n\t...props\n}: ChatInputGroupAddon) {\n\treturn <InputGroupAddon className={cn(className)} {...props} />;\n}\n\nexport type ChatInputGroupButtonProps = ComponentProps<typeof InputGroupButton>;\nexport function ChatInputGroupButton({\n\tclassName,\n\t...props\n}: ChatInputGroupButtonProps) {\n\treturn <InputGroupButton className={cn(className)} {...props} />;\n}\n\nexport type ChatInputGroupTextProps = ComponentProps<typeof InputGroupText>;\nexport function ChatInputGroupText({\n\tclassName,\n\t...props\n}: ChatInputGroupTextProps) {\n\treturn <InputGroupText className={cn(className)} {...props} />;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: Required for type inference\ntype MentionConfigsObject = Record<string, MentionConfig<any>>;\n\ntype ParsedFromObject<T extends MentionConfigsObject> = {\n\tcontent: string;\n} & {\n\t[K in keyof T]?: T[K] extends MentionConfig<infer Item> ? Item[] : never;\n};\n\ntype ParsedContentOnly = {\n\tcontent: string;\n};\n\ntype UseChatInputReturn<Mentions extends MentionConfigsObject | undefined> = {\n\tvalue: JSONContent;\n\tonChange: (value: JSONContent) => void;\n\tparsed: Mentions extends MentionConfigsObject\n\t\t? ParsedFromObject<Mentions>\n\t\t: ParsedContentOnly;\n\tclear: () => void;\n\thandleSubmit: () => void;\n} & (Mentions extends MentionConfigsObject\n\t? { mentionConfigs: Mentions }\n\t: { mentionConfigs?: never });\n\nexport function useChatInput<Mentions extends MentionConfigsObject>(config: {\n\tmentions: Mentions;\n\tinitialValue?: JSONContent;\n\tonSubmit?: (parsed: ParsedFromObject<Mentions>) => void;\n}): UseChatInputReturn<Mentions>;\n\nexport function useChatInput(config: {\n\tmentions?: never;\n\tinitialValue?: JSONContent;\n\tonSubmit?: (parsed: ParsedContentOnly) => void;\n}): UseChatInputReturn<undefined>;\n\nexport function useChatInput<\n\tMentions extends MentionConfigsObject | undefined,\n>({\n\tmentions,\n\tinitialValue,\n\tonSubmit: onCustomSubmit,\n}: {\n\tmentions?: Mentions;\n\tinitialValue?: JSONContent;\n\t// biome-ignore lint/suspicious/noExplicitAny: Required for generic config handling\n\tonSubmit?: (parsed: any) => void;\n}): UseChatInputReturn<Mentions> {\n\tconst [value, setValue] = useState<JSONContent>(\n\t\tinitialValue ?? { type: \"doc\", content: [] },\n\t);\n\n\tconst configsArray = useMemo(\n\t\t() => (mentions ? Object.values(mentions) : []),\n\t\t[mentions],\n\t);\n\n\tconst parsed = useMemo(\n\t\t() => parseContent(value, configsArray),\n\t\t[value, configsArray],\n\t);\n\n\tconst clear = useCallback(() => {\n\t\tsetValue({ type: \"doc\", content: [] });\n\t}, []);\n\n\tconst handleSubmit = useCallback(() => {\n\t\tif (parsed.content.trim().length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (onCustomSubmit) {\n\t\t\tonCustomSubmit(parsed);\n\t\t}\n\n\t\tclear();\n\t}, [parsed, onCustomSubmit, clear]);\n\n\treturn {\n\t\tvalue,\n\t\tonChange: setValue,\n\t\tparsed,\n\t\tclear,\n\t\thandleSubmit,\n\t\t...(mentions ? { mentionConfigs: mentions } : {}),\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Type inference complexity\n\t} as any;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: Required for type inference\ntype UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n\tk: infer I,\n) => void\n\t? I\n\t: never;\n\n// biome-ignore lint/suspicious/noExplicitAny: Required for type inference\ntype ConfigToField<Config extends MentionConfig<any>> =\n\tConfig extends MentionConfig<infer T>\n\t\t? { [K in Config[\"type\"]]: T[] }\n\t\t: never;\n\nexport type ParsedChatInputValue<\n\t// biome-ignore lint/suspicious/noExplicitAny: Required for type inference\n\tConfigs extends readonly MentionConfig<any>[],\n> = { content: string } & Partial<\n\tUnionToIntersection<\n\t\t{ [I in keyof Configs]: ConfigToField<Configs[I]> }[number]\n\t>\n>;\n\n// biome-ignore lint/suspicious/noExplicitAny: Required for generic config handling\nexport function parseContent<Configs extends readonly MentionConfig<any>[]>(\n\tjson: JSONContent,\n\tconfigs: Configs,\n): ParsedChatInputValue<Configs> {\n\tlet content = \"\";\n\t// biome-ignore lint/suspicious/noExplicitAny: Dynamic mention types\n\tconst mentions: Record<string, any[]> = {};\n\n\tfunction recurse(node: JSONContent) {\n\t\tif (node.type === \"text\" && node.text) {\n\t\t\tcontent += node.text;\n\t\t} else if (node.type === \"hardBreak\") {\n\t\t\tcontent += \"\\n\";\n\t\t} else if (node.type?.endsWith(\"-mention\")) {\n\t\t\tconst mentionType = node.type.slice(0, -8);\n\t\t\tconst config = configs.find((c) => c.type === mentionType);\n\t\t\tif (config) {\n\t\t\t\tconst attrs = node.attrs ?? {};\n\t\t\t\tconst id = attrs.id as string;\n\t\t\t\t//const type = attrs.type as string;\n\t\t\t\tconst label = attrs.label as string;\n\t\t\t\tcontent += `<span class=\"mention mention-${mentionType}\" data-type=\"${mentionType}\" data-id=\"${id}\" data-name=\"${label}\" >${config.trigger}${label}</span>`;\n\n\t\t\t\tif (!mentions[mentionType]) {\n\t\t\t\t\tmentions[mentionType] = [];\n\t\t\t\t}\n\t\t\t\tconst item = config.items.find((i) => i.id === id);\n\t\t\t\tif (\n\t\t\t\t\titem &&\n\t\t\t\t\t!mentions[mentionType].some(\n\t\t\t\t\t\t(existing) => existing.id === id,\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tmentions[mentionType].push(item);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcontent += node.text ?? \"\";\n\t\t\t}\n\t\t} else if (node.content) {\n\t\t\tfor (const child of node.content) {\n\t\t\t\trecurse(child);\n\t\t\t}\n\t\t\tif (node.type === \"paragraph\") {\n\t\t\t\tcontent += \"\\n\\n\";\n\t\t\t}\n\t\t}\n\t}\n\n\tif (json.content) {\n\t\tfor (const node of json.content) {\n\t\t\trecurse(node);\n\t\t}\n\t}\n\n\tcontent = content.trim();\n\n\treturn { content, ...mentions } as ParsedChatInputValue<Configs>;\n}\n",
			"type": "registry:ui"
		}
	]
}
