{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"editor","type":"registry:ui","title":"editor","description":"The Editor component is a powerful and flexible text editor that allows you to create and edit rich text content.","author":"Hayden Bleasel <hello@haydenbleasel.com>","dependencies":["@floating-ui/dom","@tiptap/core","@tiptap/extension-code-block-lowlight","@tiptap/extension-list","@tiptap/extension-subscript","@tiptap/extension-superscript","@tiptap/extension-table","@tiptap/extension-text-style","@tiptap/extension-typography","@tiptap/extensions","@tiptap/pm","@tiptap/react","@tiptap/starter-kit","@tiptap/suggestion","fuse.js","lowlight","lucide-react","tippy.js"],"devDependencies":[],"registryDependencies":["button","command","dropdown-menu","popover","separator","tooltip"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport type { Editor, Range } from \"@tiptap/core\";\nimport { mergeAttributes, Node } from \"@tiptap/core\";\nimport CodeBlockLowlight from \"@tiptap/extension-code-block-lowlight\";\nimport { TaskItem, TaskList } from \"@tiptap/extension-list\";\nimport Subscript from \"@tiptap/extension-subscript\";\nimport Superscript from \"@tiptap/extension-superscript\";\nimport {\n  Table,\n  TableCell,\n  TableHeader,\n  TableRow,\n} from \"@tiptap/extension-table\";\nimport { TextStyleKit } from \"@tiptap/extension-text-style\";\nimport Typography from \"@tiptap/extension-typography\";\nimport { CharacterCount, Placeholder } from \"@tiptap/extensions\";\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 {\n  BubbleMenu,\n  type BubbleMenuProps,\n  FloatingMenu,\n  type FloatingMenuProps,\n} from \"@tiptap/react/menus\";\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\ntype 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 type SuggestionItem = {\n  title: string;\n  description: string;\n  icon: LucideIcon;\n  searchTerms: string[];\n  command: (props: { editor: Editor; 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\n          .chain()\n          .focus()\n          .deleteRange(range)\n          .toggleList(\"taskList\", \"taskItem\")\n          .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: Editor;\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    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    }),\n  ];\n\n  return (\n    <TooltipProvider>\n      <div className={cn(className, \"[&_.ProseMirror-focused]:outline-none\")}>\n        <TiptapEditorProvider\n          editorProps={{\n            handleKeyDown: (_view, event) => {\n              handleCommandNavigation(event);\n            },\n          }}\n          extensions={[\n            ...defaultExtensions,\n            TextStyleKit,\n            ...(extensions ?? []),\n          ]}\n          immediatelyRender={false}\n          {...props}\n        />\n      </div>\n    </TooltipProvider>\n  );\n};\n\nexport type EditorFloatingMenuProps = Omit<FloatingMenuProps, \"editor\">;\n\nexport const EditorFloatingMenu = ({\n  className,\n  ...props\n}: EditorFloatingMenuProps) => {\n  const { editor } = useCurrentEditor();\n  return (\n    <FloatingMenu\n      className={cn(\"flex items-center bg-secondary\", className)}\n      editor={editor ?? null}\n      {...props}\n    />\n  );\n};\n\nexport type EditorBubbleMenuProps = Omit<BubbleMenuProps, \"editor\">;\n\nexport const EditorBubbleMenu = ({\n  className,\n  children,\n  ...props\n}: EditorBubbleMenuProps) => {\n  const { editor } = useCurrentEditor();\n  return (\n    <BubbleMenu\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      editor={editor ?? undefined}\n      {...props}\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    </BubbleMenu>\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 ${hideName ? \"\" : \"w-full\"}`}\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={() =>\n        editor.chain().focus().toggleList(\"taskList\", \"taskItem\").run()\n      }\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 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=\"secondary\">\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","target":"components/kibo-ui/editor/index.tsx"}],"css":{}}