{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-editor",
  "type": "registry:ui",
  "description": "Code editor component",
  "files": [
    {
      "path": "packages/code/editor.tsx",
      "content": "'use client';\n\nimport type { Editor as TiptapEditor, Range } from '@tiptap/core';\nimport { mergeAttributes, Node } from '@tiptap/core';\nimport CharacterCount from '@tiptap/extension-character-count';\nimport CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';\nimport Placeholder from '@tiptap/extension-placeholder';\nimport Subscript from '@tiptap/extension-subscript';\nimport Superscript from '@tiptap/extension-superscript';\nimport Underline from '@tiptap/extension-underline';\nimport Link from '@tiptap/extension-link';\nimport { TaskItem } from '@tiptap/extension-task-item';\nimport { TaskList } from '@tiptap/extension-task-list';\nimport { TextStyle } from '@tiptap/extension-text-style';\nimport Typography from '@tiptap/extension-typography';\nimport type { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model';\nimport { PluginKey } from '@tiptap/pm/state';\nimport {\n    ReactRenderer,\n    EditorProvider as TiptapEditorProvider,\n    type EditorProviderProps as TiptapEditorProviderProps,\n    useCurrentEditor,\n} from '@tiptap/react';\nimport { Button } from '@/components/ui/button';\nimport {\n    Command,\n    CommandEmpty,\n    CommandItem,\n    CommandList,\n} from '@/components/ui/command';\nimport {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport {\n    Popover,\n    PopoverContent,\n    PopoverTrigger,\n} from '@/components/ui/popover';\nimport { Separator } from '@/components/ui/separator';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport { cn } from '@/lib/utils';\n\nexport type { Editor, JSONContent } from '@tiptap/react';\n\nimport StarterKit from '@tiptap/starter-kit';\nimport Suggestion, { type SuggestionOptions } from '@tiptap/suggestion';\nimport Fuse from 'fuse.js';\nimport { all, createLowlight } from 'lowlight';\nimport {\n    ArrowDownIcon,\n    ArrowLeftIcon,\n    ArrowRightIcon,\n    ArrowUpIcon,\n    BoldIcon,\n    BoltIcon,\n    CheckIcon,\n    CheckSquareIcon,\n    ChevronDownIcon,\n    CodeIcon,\n    ColumnsIcon,\n    EllipsisIcon,\n    EllipsisVerticalIcon,\n    ExternalLinkIcon,\n    Heading1Icon,\n    Heading2Icon,\n    Heading3Icon,\n    ItalicIcon,\n    ListIcon,\n    ListOrderedIcon,\n    type LucideIcon,\n    type LucideProps,\n    RemoveFormattingIcon,\n    RowsIcon,\n    StrikethroughIcon,\n    SubscriptIcon,\n    SuperscriptIcon,\n    TableCellsMergeIcon,\n    TableColumnsSplitIcon,\n    TableIcon,\n    TextIcon,\n    TextQuoteIcon,\n    TrashIcon,\n    UnderlineIcon,\n} from 'lucide-react';\nimport type { FormEventHandler, HTMLAttributes, ReactNode } from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport tippy, { type Instance as TippyInstance } from 'tippy.js';\n\nimport { Table } from '@tiptap/extension-table'\nimport { TableRow } from '@tiptap/extension-table-row'\nimport { TableHeader } from '@tiptap/extension-table-header'\nimport { TableCell } from '@tiptap/extension-table-cell'\n\n\ninterface SlashNodeAttrs {\n    id: string | null;\n    label?: string | null;\n}\n\ntype SlashOptions<\n    SlashOptionSuggestionItem = unknown,\n    Attrs = SlashNodeAttrs,\n> = {\n    HTMLAttributes: Record<string, unknown>;\n    renderText: (props: {\n        options: SlashOptions<SlashOptionSuggestionItem, Attrs>;\n        node: ProseMirrorNode;\n    }) => string;\n    renderHTML: (props: {\n        options: SlashOptions<SlashOptionSuggestionItem, Attrs>;\n        node: ProseMirrorNode;\n    }) => DOMOutputSpec;\n    deleteTriggerWithBackspace: boolean;\n    suggestion: Omit<\n        SuggestionOptions<SlashOptionSuggestionItem, Attrs>,\n        'editor'\n    >;\n};\n\nconst SlashPluginKey = new PluginKey('slash');\n\nexport interface SuggestionItem {\n    title: string;\n    description: string;\n    icon: LucideIcon;\n    searchTerms: string[];\n    command: (props: { editor: TiptapEditor; range: Range }) => void;\n}\n\nexport const defaultSlashSuggestions: SuggestionOptions<SuggestionItem>['items'] =\n    () => [\n        {\n            title: 'Text',\n            description: 'Just start typing with plain text.',\n            searchTerms: ['p', 'paragraph'],\n            icon: TextIcon,\n            command: ({ editor, range }) => {\n                editor\n                    .chain()\n                    .focus()\n                    .deleteRange(range)\n                    .toggleNode('paragraph', 'paragraph')\n                    .run();\n            },\n        },\n        {\n            title: 'To-do List',\n            description: 'Track tasks with a to-do list.',\n            searchTerms: ['todo', 'task', 'list', 'check', 'checkbox'],\n            icon: CheckSquareIcon,\n            command: ({ editor, range }) => {\n                editor.chain().focus().deleteRange(range).toggleTaskList().run();\n            },\n        },\n        {\n            title: 'Heading 1',\n            description: 'Big section heading.',\n            searchTerms: ['title', 'big', 'large'],\n            icon: Heading1Icon,\n            command: ({ editor, range }) => {\n                editor\n                    .chain()\n                    .focus()\n                    .deleteRange(range)\n                    .setNode('heading', { level: 1 })\n                    .run();\n            },\n        },\n        {\n            title: 'Heading 2',\n            description: 'Medium section heading.',\n            searchTerms: ['subtitle', 'medium'],\n            icon: Heading2Icon,\n            command: ({ editor, range }) => {\n                editor\n                    .chain()\n                    .focus()\n                    .deleteRange(range)\n                    .setNode('heading', { level: 2 })\n                    .run();\n            },\n        },\n        {\n            title: 'Heading 3',\n            description: 'Small section heading.',\n            searchTerms: ['subtitle', 'small'],\n            icon: Heading3Icon,\n            command: ({ editor, range }) => {\n                editor\n                    .chain()\n                    .focus()\n                    .deleteRange(range)\n                    .setNode('heading', { level: 3 })\n                    .run();\n            },\n        },\n        {\n            title: 'Bullet List',\n            description: 'Create a simple bullet list.',\n            searchTerms: ['unordered', 'point'],\n            icon: ListIcon,\n            command: ({ editor, range }) => {\n                editor.chain().focus().deleteRange(range).toggleBulletList().run();\n            },\n        },\n        {\n            title: 'Numbered List',\n            description: 'Create a list with numbering.',\n            searchTerms: ['ordered'],\n            icon: ListOrderedIcon,\n            command: ({ editor, range }) => {\n                editor.chain().focus().deleteRange(range).toggleOrderedList().run();\n            },\n        },\n        {\n            title: 'Quote',\n            description: 'Capture a quote.',\n            searchTerms: ['blockquote'],\n            icon: TextQuoteIcon,\n            command: ({ editor, range }) =>\n                editor\n                    .chain()\n                    .focus()\n                    .deleteRange(range)\n                    .toggleNode('paragraph', 'paragraph')\n                    .toggleBlockquote()\n                    .run(),\n        },\n        {\n            title: 'Code',\n            description: 'Capture a code snippet.',\n            searchTerms: ['codeblock'],\n            icon: CodeIcon,\n            command: ({ editor, range }) =>\n                editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),\n        },\n        {\n            title: 'Table',\n            description: 'Add a table view to organize data.',\n            searchTerms: ['table'],\n            icon: TableIcon,\n            command: ({ editor, range }) =>\n                editor\n                    .chain()\n                    .focus()\n                    .deleteRange(range)\n                    .insertTable({ rows: 3, cols: 3, withHeaderRow: true })\n                    .run(),\n        },\n    ];\n\nconst Slash = Node.create<SlashOptions>({\n    name: 'slash',\n    priority: 101,\n    addOptions() {\n        return {\n            HTMLAttributes: {},\n            renderText({ options, node }) {\n                return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;\n            },\n            deleteTriggerWithBackspace: false,\n            renderHTML({ options, node }) {\n                return [\n                    'span',\n                    mergeAttributes(this.HTMLAttributes, options.HTMLAttributes),\n                    `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`,\n                ];\n            },\n            suggestion: {\n                char: '/',\n                pluginKey: SlashPluginKey,\n                command: ({ editor, range, props }) => {\n                    // increase range.to by one when the next node is of type \"text\"\n                    // and starts with a space character\n                    const nodeAfter = editor.view.state.selection.$to.nodeAfter;\n                    const overrideSpace = nodeAfter?.text?.startsWith(' ');\n\n                    if (overrideSpace) {\n                        range.to += 1;\n                    }\n\n                    editor\n                        .chain()\n                        .focus()\n                        .insertContentAt(range, [\n                            {\n                                type: this.name,\n                                attrs: props,\n                            },\n                            {\n                                type: 'text',\n                                text: ' ',\n                            },\n                        ])\n                        .run();\n\n                    // get reference to `window` object from editor element, to support cross-frame JS usage\n                    editor.view.dom.ownerDocument.defaultView\n                        ?.getSelection()\n                        ?.collapseToEnd();\n                },\n                allow: ({ state, range }) => {\n                    const $from = state.doc.resolve(range.from);\n                    const type = state.schema.nodes[this.name];\n                    const allow = !!$from.parent.type.contentMatch.matchType(type);\n\n                    return allow;\n                },\n            },\n        };\n    },\n\n    group: 'inline',\n\n    inline: true,\n\n    selectable: false,\n\n    atom: true,\n\n    addAttributes() {\n        return {\n            id: {\n                default: null,\n                parseHTML: (element) => element.getAttribute('data-id'),\n                renderHTML: (attributes) => {\n                    if (!attributes.id) {\n                        return {};\n                    }\n\n                    return {\n                        'data-id': attributes.id,\n                    };\n                },\n            },\n\n            label: {\n                default: null,\n                parseHTML: (element) => element.getAttribute('data-label'),\n                renderHTML: (attributes) => {\n                    if (!attributes.label) {\n                        return {};\n                    }\n\n                    return {\n                        'data-label': attributes.label,\n                    };\n                },\n            },\n        };\n    },\n\n    parseHTML() {\n        return [\n            {\n                tag: `span[data-type=\"${this.name}\"]`,\n            },\n        ];\n    },\n\n    renderHTML({ node, HTMLAttributes }) {\n        const mergedOptions = { ...this.options };\n\n        mergedOptions.HTMLAttributes = mergeAttributes(\n            { 'data-type': this.name },\n            this.options.HTMLAttributes,\n            HTMLAttributes\n        );\n        const html = this.options.renderHTML({\n            options: mergedOptions,\n            node,\n        });\n\n        if (typeof html === 'string') {\n            return [\n                'span',\n                mergeAttributes(\n                    { 'data-type': this.name },\n                    this.options.HTMLAttributes,\n                    HTMLAttributes\n                ),\n                html,\n            ];\n        }\n        return html;\n    },\n\n    renderText({ node }) {\n        return this.options.renderText({\n            options: this.options,\n            node,\n        });\n    },\n\n    addKeyboardShortcuts() {\n        return {\n            Backspace: () =>\n                this.editor.commands.command(({ tr, state }) => {\n                    let isMention = false;\n                    const { selection } = state;\n                    const { empty, anchor } = selection;\n\n                    if (!empty) {\n                        return false;\n                    }\n\n                    state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {\n                        if (node.type.name === this.name) {\n                            isMention = true;\n                            tr.insertText(\n                                this.options.deleteTriggerWithBackspace\n                                    ? ''\n                                    : this.options.suggestion.char || '',\n                                pos,\n                                pos + node.nodeSize\n                            );\n\n                            return false;\n                        }\n                    });\n\n                    return isMention;\n                }),\n        };\n    },\n\n    addProseMirrorPlugins() {\n        return [\n            Suggestion({\n                editor: this.editor,\n                ...this.options.suggestion,\n            }),\n        ];\n    },\n});\n\n// Create a lowlight instance with all languages loaded\nconst lowlight = createLowlight(all);\n\ntype EditorSlashMenuProps = {\n    items: SuggestionItem[];\n    command: (item: SuggestionItem) => void;\n    editor: TiptapEditor;\n    range: Range;\n};\n\nconst EditorSlashMenu = ({ items, editor, range }: EditorSlashMenuProps) => (\n    <Command\n        className=\"border shadow\"\n        id=\"slash-command\"\n        onKeyDown={(e) => {\n            e.stopPropagation();\n        }}\n    >\n        <CommandEmpty className=\"flex w-full items-center justify-center p-4 text-muted-foreground text-sm\">\n            <p>No results</p>\n        </CommandEmpty>\n        <CommandList>\n            {items.map((item) => (\n                <CommandItem\n                    className=\"flex items-center gap-3 pr-3\"\n                    key={item.title}\n                    onSelect={() => item.command({ editor, range })}\n                >\n                    <div className=\"flex size-9 shrink-0 items-center justify-center rounded border bg-secondary\">\n                        <item.icon className=\"text-muted-foreground\" size={16} />\n                    </div>\n                    <div className=\"flex flex-col\">\n                        <span className=\"font-medium text-sm\">{item.title}</span>\n                        <span className=\"text-muted-foreground text-xs\">\n                            {item.description}\n                        </span>\n                    </div>\n                </CommandItem>\n            ))}\n        </CommandList>\n    </Command>\n);\n\nconst handleCommandNavigation = (event: KeyboardEvent) => {\n    if (['ArrowUp', 'ArrowDown', 'Enter'].includes(event.key)) {\n        const slashCommand = document.querySelector('#slash-command');\n\n        if (slashCommand) {\n            event.preventDefault();\n\n            slashCommand.dispatchEvent(\n                new KeyboardEvent('keydown', {\n                    key: event.key,\n                    cancelable: true,\n                    bubbles: true,\n                })\n            );\n\n            return true;\n        }\n    }\n};\n\nexport type EditorProviderProps = TiptapEditorProviderProps & {\n    className?: string;\n    limit?: number;\n    placeholder?: string;\n};\n\nexport const EditorProvider = ({\n    className,\n    extensions,\n    limit,\n    placeholder,\n    ...props\n}: EditorProviderProps) => {\n    const defaultExtensions = [\n        StarterKit.configure({\n            codeBlock: false,\n            bulletList: {\n                HTMLAttributes: {\n                    class: cn('list-outside list-disc pl-4'),\n                },\n            },\n            orderedList: {\n                HTMLAttributes: {\n                    class: cn('list-outside list-decimal pl-4'),\n                },\n            },\n            listItem: {\n                HTMLAttributes: {\n                    class: cn('leading-normal'),\n                },\n            },\n            blockquote: {\n                HTMLAttributes: {\n                    class: cn('border-l border-l-2 pl-2'),\n                },\n            },\n            code: {\n                HTMLAttributes: {\n                    class: cn('rounded-md bg-muted px-1.5 py-1 font-medium font-mono'),\n                    spellcheck: 'false',\n                },\n            },\n            horizontalRule: {\n                HTMLAttributes: {\n                    class: cn('mt-4 mb-6 border-muted-foreground border-t'),\n                },\n            },\n            dropcursor: {\n                color: 'var(--border)',\n                width: 4,\n            },\n        }),\n        Typography,\n        Placeholder.configure({\n            placeholder,\n            emptyEditorClass:\n                'before:text-muted-foreground before:content-[attr(data-placeholder)] before:float-left before:h-0 before:pointer-events-none',\n        }),\n        CharacterCount.configure({\n            limit,\n        }),\n        CodeBlockLowlight.configure({\n            lowlight,\n            HTMLAttributes: {\n                class: cn(\n                    'rounded-md border p-4 text-sm',\n                    'bg-background text-foreground',\n                    '[&_.hljs-doctag]:text-[#d73a49] [&_.hljs-keyword]:text-[#d73a49] [&_.hljs-meta_.hljs-keyword]:text-[#d73a49] [&_.hljs-template-tag]:text-[#d73a49] [&_.hljs-template-variable]:text-[#d73a49] [&_.hljs-type]:text-[#d73a49] [&_.hljs-variable.language_]:text-[#d73a49]',\n                    '[&_.hljs-title.class_.inherited__]:text-[#6f42c1] [&_.hljs-title.class_]:text-[#6f42c1] [&_.hljs-title.function_]:text-[#6f42c1] [&_.hljs-title]:text-[#6f42c1]',\n                    '[&_.hljs-attr]:text-[#005cc5] [&_.hljs-attribute]:text-[#005cc5] [&_.hljs-literal]:text-[#005cc5] [&_.hljs-meta]:text-[#005cc5] [&_.hljs-number]:text-[#005cc5] [&_.hljs-operator]:text-[#005cc5] [&_.hljs-selector-attr]:text-[#005cc5] [&_.hljs-selector-class]:text-[#005cc5] [&_.hljs-selector-id]:text-[#005cc5] [&_.hljs-variable]:text-[#005cc5]',\n                    '[&_.hljs-meta_.hljs-string]:text-[#032f62] [&_.hljs-regexp]:text-[#032f62] [&_.hljs-string]:text-[#032f62]',\n                    '[&_.hljs-built_in]:text-[#e36209] [&_.hljs-symbol]:text-[#e36209]',\n                    '[&_.hljs-code]:text-[#6a737d] [&_.hljs-comment]:text-[#6a737d] [&_.hljs-formula]:text-[#6a737d]',\n                    '[&_.hljs-name]:text-[#22863a] [&_.hljs-quote]:text-[#22863a] [&_.hljs-selector-pseudo]:text-[#22863a] [&_.hljs-selector-tag]:text-[#22863a]',\n                    '[&_.hljs-subst]:text-[#24292e]',\n                    '[&_.hljs-section]:font-bold [&_.hljs-section]:text-[#005cc5]',\n                    '[&_.hljs-bullet]:text-[#735c0f]',\n                    '[&_.hljs-emphasis]:text-[#24292e] [&_.hljs-emphasis]:italic',\n                    '[&_.hljs-strong]:font-bold [&_.hljs-strong]:text-[#24292e]',\n                    '[&_.hljs-addition]:bg-[#f0fff4] [&_.hljs-addition]:text-[#22863a]',\n                    '[&_.hljs-deletion]:bg-[#ffeef0] [&_.hljs-deletion]:text-[#b31d28]'\n                ),\n            },\n        }),\n        Superscript,\n        Subscript,\n        Underline,\n        Link.configure({\n            autolink: true,\n            openOnClick: false,\n            linkOnPaste: true,\n            HTMLAttributes: { rel: 'noreferrer noopener', target: '_blank' },\n            validate: (href: string) => {\n                try {\n                    new URL(href);\n                    return true;\n                } catch {\n                    return false;\n                }\n            },\n        }),\n        Slash.configure({\n            suggestion: {\n                items: async ({ editor, query }) => {\n                    const items = await defaultSlashSuggestions({ editor, query });\n\n                    if (!query) {\n                        return items;\n                    }\n\n                    const slashFuse = new Fuse(items, {\n                        keys: ['title', 'description', 'searchTerms'],\n                        threshold: 0.2,\n                        minMatchCharLength: 1,\n                    });\n\n                    const results = slashFuse.search(query);\n\n                    return results.map((result) => result.item);\n                },\n                char: '/',\n                render: () => {\n                    let component: ReactRenderer<EditorSlashMenuProps>;\n                    let popup: TippyInstance;\n\n                    return {\n                        onStart: (onStartProps) => {\n                            component = new ReactRenderer(EditorSlashMenu, {\n                                props: onStartProps,\n                                editor: onStartProps.editor,\n                            });\n\n                            popup = tippy(document.body, {\n                                getReferenceClientRect: () =>\n                                    onStartProps.clientRect?.() || new DOMRect(),\n                                appendTo: () => document.body,\n                                content: component.element,\n                                showOnCreate: true,\n                                interactive: true,\n                                trigger: 'manual',\n                                placement: 'bottom-start',\n                            });\n                        },\n\n                        onUpdate(onUpdateProps) {\n                            component.updateProps(onUpdateProps);\n\n                            popup.setProps({\n                                getReferenceClientRect: () =>\n                                    onUpdateProps.clientRect?.() || new DOMRect(),\n                            });\n                        },\n\n                        onKeyDown(onKeyDownProps) {\n                            if (onKeyDownProps.event.key === 'Escape') {\n                                popup.hide();\n                                component.destroy();\n\n                                return true;\n                            }\n\n                            return handleCommandNavigation(onKeyDownProps.event) ?? false;\n                        },\n\n                        onExit() {\n                            popup.destroy();\n                            component.destroy();\n                        },\n                    };\n                },\n            },\n        }),\n        Table.configure({\n            HTMLAttributes: {\n                class: cn(\n                    'relative m-0 mx-auto my-3 w-full table-fixed border-collapse overflow-hidden rounded-none text-sm'\n                ),\n            },\n            allowTableNodeSelection: true,\n        }),\n        TableRow.configure({\n            HTMLAttributes: {\n                class: cn(\n                    'relative box-border min-w-[1em] border p-1 text-start align-top'\n                ),\n            },\n        }),\n        TableCell.configure({\n            HTMLAttributes: {\n                class: cn(\n                    'relative box-border min-w-[1em] border p-1 text-start align-top'\n                ),\n            },\n        }),\n        TableHeader.configure({\n            HTMLAttributes: {\n                class: cn(\n                    'relative box-border min-w-[1em] border bg-secondary p-1 text-start align-top font-medium font-semibold text-muted-foreground'\n                ),\n            },\n        }),\n        TaskList.configure({\n            HTMLAttributes: {\n                // 17px = the width of the checkbox + the gap between the checkbox and the text\n                class: 'before:translate-x-[17px]',\n            },\n        }),\n        TaskItem.configure({\n            HTMLAttributes: {\n                class: 'flex items-start gap-1',\n            },\n            nested: true,\n        }),\n        TextStyle.configure({ mergeNestedSpanStyles: true }),\n    ];\n\n    return (\n        <TooltipProvider>\n            <div className={cn(className, '[&_.ProseMirror-focused]:outline-none')}>\n                <TiptapEditorProvider\n                    // avoid re-rendering the editor on every keystroke\n                    // this is a performance optimization\n                    // see https://tiptap.dev/api/components/editor-provider#immediately-render\n                    immediatelyRender={false}\n                    editorProps={{\n                        handleKeyDown: (_view, event) => {\n                            handleCommandNavigation(event);\n                        },\n                    }}\n                    extensions={[\n                        ...defaultExtensions,\n                        ...(extensions ?? [])]}\n                    {...props}\n                />\n            </div>\n        </TooltipProvider>\n    );\n};\n\nexport type EditorFloatingMenuProps = {\n    className?: string;\n    children?: React.ReactNode;\n};\n\nexport const EditorFloatingMenu = ({\n    className,\n    children,\n}: EditorFloatingMenuProps) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <div className={cn('flex items-center bg-secondary', className)}>\n            {children}\n        </div>\n    );\n};\n\nexport type EditorBubbleMenuProps = {\n    className?: string;\n    children?: React.ReactNode;\n};\n\nexport const EditorBubbleMenu = ({\n    className,\n    children,\n}: EditorBubbleMenuProps) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <div\n            className={cn(\n                'flex rounded-xl border bg-background p-0.5 shadow',\n                '[&>*:first-child]:rounded-l-[9px]',\n                '[&>*:last-child]:rounded-r-[9px]',\n                className\n            )}\n        >\n            {children && Array.isArray(children)\n                ? children.reduce((acc: ReactNode[], child, index) => {\n                    if (index === 0) {\n                        return [child];\n                    }\n\n                    // biome-ignore lint/suspicious/noArrayIndexKey: \"only iterator we have\"\n                    acc.push(<Separator key={index} orientation=\"vertical\" />);\n                    acc.push(child);\n                    return acc;\n                }, [])\n                : children}\n        </div>\n    );\n};\n\ntype EditorButtonProps = {\n    name: string;\n    isActive: () => boolean;\n    command: () => void;\n    icon: LucideIcon | ((props: LucideProps) => ReactNode);\n    hideName?: boolean;\n};\n\nconst BubbleMenuButton = ({\n    name,\n    isActive,\n    command,\n    icon: Icon,\n    hideName,\n}: EditorButtonProps) => (\n    <Button\n        className=\"flex gap-4\"\n        onClick={() => command()}\n        size=\"sm\"\n        variant=\"ghost\"\n    >\n        <Icon className=\"shrink-0 text-muted-foreground\" size={12} />\n        {!hideName && <span className=\"flex-1 text-left\">{name}</span>}\n        {isActive() ? (\n            <CheckIcon className=\"shrink-0 text-muted-foreground\" size={12} />\n        ) : null}\n    </Button>\n);\n\nexport type EditorClearFormattingProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorClearFormatting = ({\n    hideName = true,\n}: EditorClearFormattingProps) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().clearNodes().unsetAllMarks().run()}\n            hideName={hideName}\n            icon={RemoveFormattingIcon}\n            isActive={() => false}\n            name=\"Clear Formatting\"\n        />\n    );\n};\n\nexport type EditorNodeTextProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeText = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() =>\n                editor.chain().focus().toggleNode('paragraph', 'paragraph').run()\n            }\n            hideName={hideName}\n            // I feel like there has to be a more efficient way to do this – feel free to PR if you know how!\n            icon={TextIcon}\n            isActive={() =>\n                (editor &&\n                    !editor.isActive('paragraph') &&\n                    !editor.isActive('bulletList') &&\n                    !editor.isActive('orderedList')) ??\n                false\n            }\n            name=\"Text\"\n        />\n    );\n};\n\nexport type EditorNodeHeading1Props = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeHeading1 = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}\n            hideName={hideName}\n            icon={Heading1Icon}\n            isActive={() => editor.isActive('heading', { level: 1 }) ?? false}\n            name=\"Heading 1\"\n        />\n    );\n};\n\nexport type EditorNodeHeading2Props = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeHeading2 = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}\n            hideName={hideName}\n            icon={Heading2Icon}\n            isActive={() => editor.isActive('heading', { level: 2 }) ?? false}\n            name=\"Heading 2\"\n        />\n    );\n};\n\nexport type EditorNodeHeading3Props = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeHeading3 = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}\n            hideName={hideName}\n            icon={Heading3Icon}\n            isActive={() => editor.isActive('heading', { level: 3 }) ?? false}\n            name=\"Heading 3\"\n        />\n    );\n};\n\nexport type EditorNodeBulletListProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeBulletList = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleBulletList().run()}\n            hideName={hideName}\n            icon={ListIcon}\n            isActive={() => editor.isActive('bulletList') ?? false}\n            name=\"Bullet List\"\n        />\n    );\n};\n\nexport type EditorNodeOrderedListProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeOrderedList = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleOrderedList().run()}\n            hideName={hideName}\n            icon={ListOrderedIcon}\n            isActive={() => editor.isActive('orderedList') ?? false}\n            name=\"Numbered List\"\n        />\n    );\n};\n\nexport type EditorNodeTaskListProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeTaskList = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleTaskList().run()}\n            hideName={hideName}\n            icon={CheckSquareIcon}\n            isActive={() => editor.isActive('taskItem') ?? false}\n            name=\"To-do List\"\n        />\n    );\n};\n\nexport type EditorNodeQuoteProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeQuote = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() =>\n                editor\n                    .chain()\n                    .focus()\n                    .toggleNode('paragraph', 'paragraph')\n                    .toggleBlockquote()\n                    .run()\n            }\n            hideName={hideName}\n            icon={TextQuoteIcon}\n            isActive={() => editor.isActive('blockquote') ?? false}\n            name=\"Quote\"\n        />\n    );\n};\n\nexport type EditorNodeCodeProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeCode = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleCodeBlock().run()}\n            hideName={hideName}\n            icon={CodeIcon}\n            isActive={() => editor.isActive('codeBlock') ?? false}\n            name=\"Code\"\n        />\n    );\n};\n\nexport type EditorNodeTableProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorNodeTable = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() =>\n                editor\n                    .chain()\n                    .focus()\n                    .insertTable({ rows: 3, cols: 3, withHeaderRow: true })\n                    .run()\n            }\n            hideName={hideName}\n            icon={TableIcon}\n            isActive={() => editor.isActive('table') ?? false}\n            name=\"Table\"\n        />\n    );\n};\n\nexport type EditorSelectorProps = HTMLAttributes<HTMLDivElement> & {\n    open?: boolean;\n    onOpenChange?: (open: boolean) => void;\n    title: string;\n};\n\nexport const EditorSelector = ({\n    open,\n    onOpenChange,\n    title,\n    className,\n    children,\n    ...props\n}: EditorSelectorProps) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <Popover modal onOpenChange={onOpenChange} open={open}>\n            <PopoverTrigger asChild>\n                <Button\n                    className=\"gap-2 rounded-none border-none\"\n                    size=\"sm\"\n                    variant=\"ghost\"\n                >\n                    <span className=\"whitespace-nowrap text-xs\">{title}</span>\n                    <ChevronDownIcon size={12} />\n                </Button>\n            </PopoverTrigger>\n            <PopoverContent\n                align=\"start\"\n                className={cn('w-48 p-1', className)}\n                sideOffset={5}\n                {...props}\n            >\n                {children}\n            </PopoverContent>\n        </Popover>\n    );\n};\n\nexport type EditorFormatBoldProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorFormatBold = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleBold().run()}\n            hideName={hideName}\n            icon={BoldIcon}\n            isActive={() => editor.isActive('bold') ?? false}\n            name=\"Bold\"\n        />\n    );\n};\n\nexport type EditorFormatItalicProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorFormatItalic = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleItalic().run()}\n            hideName={hideName}\n            icon={ItalicIcon}\n            isActive={() => editor.isActive('italic') ?? false}\n            name=\"Italic\"\n        />\n    );\n};\n\nexport type EditorFormatStrikeProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorFormatStrike = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleStrike().run()}\n            hideName={hideName}\n            icon={StrikethroughIcon}\n            isActive={() => editor.isActive('strike') ?? false}\n            name=\"Strikethrough\"\n        />\n    );\n};\n\nexport type EditorFormatCodeProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorFormatCode = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleCode().run()}\n            hideName={hideName}\n            icon={CodeIcon}\n            isActive={() => editor.isActive('code') ?? false}\n            name=\"Code\"\n        />\n    );\n};\n\nexport type EditorFormatSubscriptProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorFormatSubscript = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleSubscript().run()}\n            hideName={hideName}\n            icon={SubscriptIcon}\n            isActive={() => editor.isActive('subscript') ?? false}\n            name=\"Subscript\"\n        />\n    );\n};\n\nexport type EditorFormatSuperscriptProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorFormatSuperscript = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleSuperscript().run()}\n            hideName={hideName}\n            icon={SuperscriptIcon}\n            isActive={() => editor.isActive('superscript') ?? false}\n            name=\"Superscript\"\n        />\n    );\n};\n\nexport type EditorFormatUnderlineProps = Pick<EditorButtonProps, 'hideName'>;\n\nexport const EditorFormatUnderline = ({\n    hideName = false,\n}: Pick<EditorButtonProps, 'hideName'>) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <BubbleMenuButton\n            command={() => editor.chain().focus().toggleUnderline().run()}\n            hideName={hideName}\n            icon={UnderlineIcon}\n            isActive={() => editor.isActive('underline') ?? false}\n            name=\"Underline\"\n        />\n    );\n};\n\nexport type EditorLinkSelectorProps = {\n    open?: boolean;\n    onOpenChange?: (open: boolean) => void;\n};\n\nexport const EditorLinkSelector = ({\n    open,\n    onOpenChange,\n}: EditorLinkSelectorProps) => {\n    const [url, setUrl] = useState<string>('');\n    const inputReference = useRef<HTMLInputElement>(null);\n    const { editor } = useCurrentEditor();\n\n    const isValidUrl = (text: string): boolean => {\n        try {\n            new URL(text);\n            return true;\n        } catch {\n            return false;\n        }\n    };\n\n    const getUrlFromString = (text: string): string | null => {\n        if (isValidUrl(text)) {\n            return text;\n        }\n        try {\n            if (text.includes('.') && !text.includes(' ')) {\n                return new URL(`https://${text}`).toString();\n            }\n\n            return null;\n        } catch {\n            return null;\n        }\n    };\n\n    useEffect(() => {\n        inputReference.current?.focus();\n    }, []);\n\n    if (!editor) {\n        return null;\n    }\n\n    const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {\n        event.preventDefault();\n\n        const href = getUrlFromString(url);\n\n        if (href) {\n            editor.chain().focus().setLink({ href }).run();\n            onOpenChange?.(false);\n        }\n    };\n\n    const defaultValue = (editor.getAttributes('link') as { href?: string }).href;\n\n    return (\n        <Popover modal onOpenChange={onOpenChange} open={open}>\n            <PopoverTrigger asChild>\n                <Button\n                    className=\"gap-2 rounded-none border-none\"\n                    size=\"sm\"\n                    variant=\"ghost\"\n                >\n                    <ExternalLinkIcon size={12} />\n                    <p\n                        className={cn(\n                            'text-xs underline decoration-text-muted underline-offset-4',\n                            {\n                                'text-primary': editor.isActive('link'),\n                            }\n                        )}\n                    >\n                        Link\n                    </p>\n                </Button>\n            </PopoverTrigger>\n            <PopoverContent align=\"start\" className=\"w-60 p-0\" sideOffset={10}>\n                <form className=\"flex p-1\" onSubmit={handleSubmit}>\n                    <input\n                        aria-label=\"Link URL\"\n                        className=\"flex-1 bg-background p-1 text-sm outline-none\"\n                        defaultValue={defaultValue ?? ''}\n                        onChange={(event) => setUrl(event.target.value)}\n                        placeholder=\"Paste a link\"\n                        ref={inputReference}\n                        type=\"text\"\n                        value={url}\n                    />\n                    {editor.getAttributes('link').href ? (\n                        <Button\n                            className=\"flex h-8 items-center rounded-sm p-1 text-destructive transition-all hover:bg-destructive-foreground dark:hover:bg-destructive\"\n                            onClick={() => {\n                                editor.chain().focus().unsetLink().run();\n                                onOpenChange?.(false);\n                            }}\n                            size=\"icon\"\n                            type=\"button\"\n                            variant=\"outline\"\n                        >\n                            <TrashIcon size={12} />\n                        </Button>\n                    ) : (\n                        <Button className=\"h-8\" size=\"icon\" variant=\"outline\">\n                            <CheckIcon size={12} />\n                        </Button>\n                    )}\n                </form>\n            </PopoverContent>\n        </Popover>\n    );\n};\n\nexport type EditorTableMenuProps = {\n    children: ReactNode;\n};\n\nexport const EditorTableMenu = ({ children }: EditorTableMenuProps) => {\n    const { editor } = useCurrentEditor();\n\n    if (!editor) {\n        return null;\n    }\n\n    const isActive = editor.isActive('table');\n\n    return (\n        <div\n            className={cn({\n                hidden: !isActive,\n            })}\n        >\n            {children}\n        </div>\n    );\n};\n\nexport type EditorTableGlobalMenuProps = {\n    children: ReactNode;\n};\n\nexport const EditorTableGlobalMenu = ({\n    children,\n}: EditorTableGlobalMenuProps) => {\n    const { editor } = useCurrentEditor();\n    const [top, setTop] = useState(0);\n    const [left, setLeft] = useState(0);\n\n    useEffect(() => {\n        if (!editor) {\n            return;\n        }\n\n        editor.on('selectionUpdate', () => {\n            const selection = window.getSelection();\n\n            if (!selection) {\n                return;\n            }\n\n            const range = selection.getRangeAt(0);\n            let startContainer = range.startContainer as HTMLElement | string;\n\n            if (!(startContainer instanceof HTMLElement)) {\n                startContainer = range.startContainer.parentElement as HTMLElement;\n            }\n\n            const tableNode = startContainer.closest('table');\n\n            if (!tableNode) {\n                return;\n            }\n\n            const tableRect = tableNode.getBoundingClientRect();\n\n            setTop(tableRect.top + tableRect.height);\n            setLeft(tableRect.left + tableRect.width / 2);\n        });\n\n        return () => {\n            editor.off('selectionUpdate');\n        };\n    }, [editor]);\n\n    return (\n        <div\n            className={cn(\n                '-translate-x-1/2 absolute flex translate-y-1/2 items-center rounded-full border bg-background shadow-xl',\n                {\n                    hidden: !(left || top),\n                }\n            )}\n            style={{ top, left }}\n        >\n            {children}\n        </div>\n    );\n};\n\nexport type EditorTableColumnMenuProps = {\n    children: ReactNode;\n};\n\nexport const EditorTableColumnMenu = ({\n    children,\n}: EditorTableColumnMenuProps) => {\n    const { editor } = useCurrentEditor();\n    const [top, setTop] = useState(0);\n    const [left, setLeft] = useState(0);\n\n    useEffect(() => {\n        if (!editor) {\n            return;\n        }\n\n        editor.on('selectionUpdate', () => {\n            const selection = window.getSelection();\n\n            if (!selection) {\n                return;\n            }\n\n            const range = selection.getRangeAt(0);\n            let startContainer = range.startContainer as HTMLElement | string;\n\n            if (!(startContainer instanceof HTMLElement)) {\n                startContainer = range.startContainer.parentElement as HTMLElement;\n            }\n\n            // Get the closest table cell (td or th)\n            const tableCell = startContainer.closest('td, th');\n\n            if (!tableCell) {\n                return;\n            }\n\n            const cellRect = tableCell.getBoundingClientRect();\n\n            setTop(cellRect.top);\n            setLeft(cellRect.left + cellRect.width / 2);\n        });\n\n        return () => {\n            editor.off('selectionUpdate');\n        };\n    }, [editor]);\n\n    return (\n        <DropdownMenu>\n            <DropdownMenuTrigger\n                asChild\n                className={cn(\n                    '-translate-x-1/2 -translate-y-1/2 absolute flex h-4 w-7 overflow-hidden rounded-md border bg-background shadow-xl',\n                    {\n                        hidden: !(left || top),\n                    }\n                )}\n                style={{ top, left }}\n            >\n                <Button size=\"icon\" variant=\"ghost\">\n                    <EllipsisIcon className=\"text-muted-foreground\" size={16} />\n                </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent>{children}</DropdownMenuContent>\n        </DropdownMenu>\n    );\n};\n\nexport type EditorTableRowMenuProps = {\n    children: ReactNode;\n};\n\nexport const EditorTableRowMenu = ({ children }: EditorTableRowMenuProps) => {\n    const { editor } = useCurrentEditor();\n    const [top, setTop] = useState(0);\n    const [left, setLeft] = useState(0);\n\n    useEffect(() => {\n        if (!editor) {\n            return;\n        }\n\n        editor.on('selectionUpdate', () => {\n            const selection = window.getSelection();\n\n            if (!selection) {\n                return;\n            }\n\n            const range = selection.getRangeAt(0);\n            let startContainer = range.startContainer as HTMLElement | string;\n\n            if (!(startContainer instanceof HTMLElement)) {\n                startContainer = range.startContainer.parentElement as HTMLElement;\n            }\n\n            const tableRow = startContainer.closest('tr');\n\n            if (!tableRow) {\n                return;\n            }\n\n            const rowRect = tableRow.getBoundingClientRect();\n\n            setTop(rowRect.top + rowRect.height / 2);\n            setLeft(rowRect.left);\n        });\n\n        return () => {\n            editor.off('selectionUpdate');\n        };\n    }, [editor]);\n\n    return (\n        <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n                <Button\n                    className={cn(\n                        '-translate-x-1/2 -translate-y-1/2 absolute flex h-7 w-4 overflow-hidden rounded-md border bg-background shadow-xl',\n                        {\n                            hidden: !(left || top),\n                        }\n                    )}\n                    size=\"icon\"\n                    style={{ top, left }}\n                    variant=\"ghost\"\n                >\n                    <EllipsisVerticalIcon className=\"text-muted-foreground\" size={12} />\n                </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent>{children}</DropdownMenuContent>\n        </DropdownMenu>\n    );\n};\n\nexport const EditorTableColumnBefore = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().addColumnBefore().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <DropdownMenuItem className=\"flex items-center gap-2\" onClick={handleClick}>\n            <ArrowLeftIcon className=\"text-muted-foreground\" size={16} />\n            <span>Add column before</span>\n        </DropdownMenuItem>\n    );\n};\n\nexport const EditorTableColumnAfter = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().addColumnAfter().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <DropdownMenuItem className=\"flex items-center gap-2\" onClick={handleClick}>\n            <ArrowRightIcon className=\"text-muted-foreground\" size={16} />\n            <span>Add column after</span>\n        </DropdownMenuItem>\n    );\n};\n\nexport const EditorTableRowBefore = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().addRowBefore().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <DropdownMenuItem className=\"flex items-center gap-2\" onClick={handleClick}>\n            <ArrowUpIcon className=\"text-muted-foreground\" size={16} />\n            <span>Add row before</span>\n        </DropdownMenuItem>\n    );\n};\n\nexport const EditorTableRowAfter = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().addRowAfter().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <DropdownMenuItem className=\"flex items-center gap-2\" onClick={handleClick}>\n            <ArrowDownIcon className=\"text-muted-foreground\" size={16} />\n            <span>Add row after</span>\n        </DropdownMenuItem>\n    );\n};\n\nexport const EditorTableColumnDelete = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().deleteColumn().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <DropdownMenuItem className=\"flex items-center gap-2\" onClick={handleClick}>\n            <TrashIcon className=\"text-destructive\" size={16} />\n            <span>Delete column</span>\n        </DropdownMenuItem>\n    );\n};\n\nexport const EditorTableRowDelete = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().deleteRow().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <DropdownMenuItem className=\"flex items-center gap-2\" onClick={handleClick}>\n            <TrashIcon className=\"text-destructive\" size={16} />\n            <span>Delete row</span>\n        </DropdownMenuItem>\n    );\n};\n\nexport const EditorTableHeaderColumnToggle = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().toggleHeaderColumn().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <Tooltip>\n            <TooltipTrigger asChild>\n                <Button\n                    className=\"flex items-center gap-2 rounded-full\"\n                    onClick={handleClick}\n                    size=\"icon\"\n                    variant=\"ghost\"\n                >\n                    <ColumnsIcon className=\"text-muted-foreground\" size={16} />\n                </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n                <span>Toggle header column</span>\n            </TooltipContent>\n        </Tooltip>\n    );\n};\n\nexport const EditorTableHeaderRowToggle = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().toggleHeaderRow().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <Tooltip>\n            <TooltipTrigger asChild>\n                <Button\n                    className=\"flex items-center gap-2 rounded-full\"\n                    onClick={handleClick}\n                    size=\"icon\"\n                    variant=\"ghost\"\n                >\n                    <RowsIcon className=\"text-muted-foreground\" size={16} />\n                </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n                <span>Toggle header row</span>\n            </TooltipContent>\n        </Tooltip>\n    );\n};\n\nexport const EditorTableDelete = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().deleteTable().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <Tooltip>\n            <TooltipTrigger asChild>\n                <Button\n                    className=\"flex items-center gap-2 rounded-full\"\n                    onClick={handleClick}\n                    size=\"icon\"\n                    variant=\"ghost\"\n                >\n                    <TrashIcon className=\"text-destructive\" size={16} />\n                </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n                <span>Delete table</span>\n            </TooltipContent>\n        </Tooltip>\n    );\n};\n\nexport const EditorTableMergeCells = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().mergeCells().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <Tooltip>\n            <TooltipTrigger asChild>\n                <Button\n                    className=\"flex items-center gap-2 rounded-full\"\n                    onClick={handleClick}\n                    size=\"icon\"\n                    variant=\"ghost\"\n                >\n                    <TableCellsMergeIcon className=\"text-muted-foreground\" size={16} />\n                </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n                <span>Merge cells</span>\n            </TooltipContent>\n        </Tooltip>\n    );\n};\n\nexport const EditorTableSplitCell = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().splitCell().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <Tooltip>\n            <TooltipTrigger asChild>\n                <Button\n                    className=\"flex items-center gap-2 rounded-full\"\n                    onClick={handleClick}\n                    size=\"icon\"\n                    variant=\"ghost\"\n                >\n                    <TableColumnsSplitIcon className=\"text-muted-foreground\" size={16} />\n                </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n                <span>Split cell</span>\n            </TooltipContent>\n        </Tooltip>\n    );\n};\n\nexport const EditorTableFix = () => {\n    const { editor } = useCurrentEditor();\n\n    const handleClick = useCallback(() => {\n        if (editor) {\n            editor.chain().focus().fixTables().run();\n        }\n    }, [editor]);\n\n    if (!editor) {\n        return null;\n    }\n\n    return (\n        <Tooltip>\n            <TooltipTrigger asChild>\n                <Button\n                    className=\"flex items-center gap-2 rounded-full\"\n                    onClick={handleClick}\n                    size=\"icon\"\n                    variant=\"ghost\"\n                >\n                    <BoltIcon className=\"text-muted-foreground\" size={16} />\n                </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n                <span>Fix table</span>\n            </TooltipContent>\n        </Tooltip>\n    );\n};\n\nexport type EditorCharacterCountProps = {\n    children: ReactNode;\n    className?: string;\n};\n\nexport const EditorCharacterCount = {\n    Characters({ children, className }: EditorCharacterCountProps) {\n        const { editor } = useCurrentEditor();\n\n        if (!editor) {\n            return null;\n        }\n\n        return (\n            <div\n                className={cn(\n                    'absolute right-4 bottom-4 rounded-md border bg-background p-2 text-muted-foreground text-sm shadow',\n                    className\n                )}\n            >\n                {children}\n                {editor.storage.characterCount.characters()}\n            </div>\n        );\n    },\n\n    Words({ children, className }: EditorCharacterCountProps) {\n        const { editor } = useCurrentEditor();\n\n        if (!editor) {\n            return null;\n        }\n\n        return (\n            <div\n                className={cn(\n                    'absolute right-4 bottom-4 rounded-md border bg-background p-2 text-muted-foreground text-sm shadow',\n                    className\n                )}\n            >\n                {children}\n                {editor.storage.characterCount.words()}\n            </div>\n        );\n    },\n};\n",
      "type": "registry:component"
    }
  ]
}