{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "dependencies": [
    "@tiptap/core@>=3.30.5 <4",
    "@tiptap/react@>=2.11.5 <4",
    "@tiptap/pm@>=2.11.5 <4",
    "@tiptap/starter-kit@>=2.11.5 <4",
    "@tiptap/extension-link@>=2.11.5 <4",
    "@tiptap/extension-underline@>=2.11.5 <4",
    "@tiptap/extension-highlight@>=2.11.5 <4",
    "@tiptap/extension-text-align@>=2.11.5 <4",
    "@tiptap/extension-subscript@>=2.11.5 <4",
    "@tiptap/extension-superscript@>=2.11.5 <4",
    "@tiptap/extension-placeholder@>=2.11.5 <4",
    "@tiptap/extension-character-count@>=2.11.5 <4",
    "@tiptap/extension-text-style@>=2.11.5 <4",
    "@tiptap/extension-color@>=2.11.5 <4",
    "@base-ui/react@^1.0.0",
    "class-variance-authority@^0.7.1",
    "clsx@^2.1.1",
    "lucide-react@>=0.400.0 <1.0.0",
    "tailwind-merge@^3.0.0"
  ],
  "description": "A toolbar-style rich text editor built on Tiptap with shadcn/ui tokens.",
  "files": [
    {
      "content": "export { RichTextEditor } from \"./rte-text-editor\";\nexport { useRichTextEditorContext } from \"./rte-context\";\nexport { BubbleMenu } from \"./bubble-menu/index\";\nexport type { RichTextEditorProps } from \"./types\";\nexport type { RichTextEditorVariant } from \"./types\";\nexport type { RichTextEditorFooterProps, WordCountInfo } from \"./types\";\nexport type { RichTextEditorLabels } from \"./labels\";\nexport { DEFAULT_LABELS } from \"./labels\";\nexport type { RichTextEditorIcons } from \"./icon-types\";\nexport { DEFAULT_ICONS } from \"./icons\";\nexport { DEFAULT_LANGUAGE_ICONS } from \"./language-icons\";\nexport { Link } from \"./extensions\";\nexport { TwitterEmbed } from \"./controls/rte-twitter-control\";\nexport { YouTubeEmbed } from \"./controls/rte-youtube-control\";\n",
      "path": "editor/index.ts",
      "target": "@components/editor/index.ts",
      "type": "registry:component"
    },
    {
      "content": "import { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport { useEffect, useMemo, useState } from \"react\";\n\nimport { BubbleMenu } from \"./bubble-menu/index\";\nimport {\n  RichTextEditorControl,\n  RichTextEditorTooltip,\n} from \"./controls/rte-control\";\nimport * as controls from \"./controls/rte-controls\";\nimport { LinkControl } from \"./controls/rte-link-control\";\nimport { TwitterEmbedControl } from \"./controls/rte-twitter-control\";\nimport { YouTubeEmbedControl } from \"./controls/rte-youtube-control\";\nimport { DEFAULT_ICONS } from \"./icons\";\nimport { DEFAULT_LABELS } from \"./labels\";\nimport { Content } from \"./rte-content\";\nimport { RichTextEditorContext } from \"./rte-context\";\nimport { ControlsGroup } from \"./rte-controls-group\";\nimport { Footer } from \"./rte-footer\";\nimport { Toolbar } from \"./rte-toolbar\";\nimport type { RichTextEditorProps } from \"./types\";\nimport { Tooltip, TooltipContent, TooltipProvider } from \"./ui/tooltip\";\nimport { cn } from \"./ui/utils\";\n\nconst RichTextEditorRoot = ({\n  editor,\n  children,\n  className,\n  labels,\n  icons,\n  variant = \"default\",\n  editable = true,\n}: RichTextEditorProps) => {\n  const mergedLabels = useMemo(\n    () => ({ ...DEFAULT_LABELS, ...labels }),\n    [labels]\n  );\n\n  const mergedIcons = useMemo(() => ({ ...DEFAULT_ICONS, ...icons }), [icons]);\n  const [tooltip] = useState(() => TooltipPrimitive.createHandle<string>());\n\n  useEffect(() => {\n    if (editor && editor.isEditable !== editable) {\n      editor.setEditable(editable);\n    }\n  }, [editor, editable]);\n\n  return (\n    <RichTextEditorContext.Provider\n      value={{\n        editable,\n        editor,\n        icons: mergedIcons,\n        labels: mergedLabels,\n        tooltip,\n        variant,\n      }}\n    >\n      <div\n        className={cn(\n          \"rte-root\",\n          variant !== \"default\" && `rte-root--${variant}`,\n          className\n        )}\n        data-variant={variant}\n      >\n        <TooltipProvider>\n          {children}\n          <Tooltip handle={tooltip}>\n            {({ payload }) => (\n              <TooltipContent side=\"bottom\">{payload as string}</TooltipContent>\n            )}\n          </Tooltip>\n        </TooltipProvider>\n      </div>\n    </RichTextEditorContext.Provider>\n  );\n};\n\nexport const RichTextEditor = Object.assign(RichTextEditorRoot, {\n  AlignCenter: controls.AlignCenterControl,\n  AlignJustify: controls.AlignJustifyControl,\n  AlignLeft: controls.AlignLeftControl,\n  AlignRight: controls.AlignRightControl,\n  Blockquote: controls.BlockquoteControl,\n  Bold: controls.BoldControl,\n  BubbleMenu,\n  BulletList: controls.BulletListControl,\n  ClearFormatting: controls.ClearFormattingControl,\n  Code: controls.CodeControl,\n  CodeBlock: controls.CodeBlockControl,\n  Content,\n  Control: RichTextEditorControl,\n  ControlsGroup,\n  Footer,\n  H1: controls.H1Control,\n  H2: controls.H2Control,\n  H3: controls.H3Control,\n  H4: controls.H4Control,\n  H5: controls.H5Control,\n  H6: controls.H6Control,\n  Highlight: controls.HighlightControl,\n  Hr: controls.HrControl,\n  Italic: controls.ItalicControl,\n  Link: LinkControl,\n  OrderedList: controls.OrderedListControl,\n  Redo: controls.RedoControl,\n  Strikethrough: controls.StrikethroughControl,\n  Subscript: controls.SubscriptControl,\n  Superscript: controls.SuperscriptControl,\n  Toolbar,\n  Tooltip: RichTextEditorTooltip,\n  TwitterEmbed: TwitterEmbedControl,\n  Underline: controls.UnderlineControl,\n  Undo: controls.UndoControl,\n  Unlink: controls.UnlinkControl,\n  YouTubeEmbed: YouTubeEmbedControl,\n});\n",
      "path": "editor/rte-text-editor.tsx",
      "target": "@components/editor/rte-text-editor.tsx",
      "type": "registry:component"
    },
    {
      "content": "import type { Tooltip } from \"@base-ui/react/tooltip\";\nimport type { Editor } from \"@tiptap/react\";\nimport { createContext, useContext } from \"react\";\n\nimport type { RichTextEditorIcons } from \"./icon-types\";\nimport type { RichTextEditorLabels } from \"./labels\";\nimport type { RichTextEditorVariant } from \"./types\";\n\ninterface RichTextEditorContextValue {\n  editor: Editor | null;\n  labels: RichTextEditorLabels;\n  icons: RichTextEditorIcons;\n  variant: RichTextEditorVariant;\n  editable: boolean;\n  tooltip: Tooltip.Handle<string>;\n}\n\nexport const RichTextEditorContext =\n  createContext<RichTextEditorContextValue | null>(null);\n\nexport const useRichTextEditorContext = (): RichTextEditorContextValue => {\n  const context = useContext(RichTextEditorContext);\n  if (!context) {\n    throw new Error(\n      \"useRichTextEditorContext must be used within RichTextEditor\"\n    );\n  }\n  return context;\n};\n",
      "path": "editor/rte-context.ts",
      "target": "@components/editor/rte-context.ts",
      "type": "registry:component"
    },
    {
      "content": "import { useRichTextEditorContext } from \"./rte-context\";\nimport type { RichTextEditorToolbarProps } from \"./types\";\nimport { cn } from \"./ui/utils\";\n\nexport const Toolbar = ({\n  children,\n  className,\n  sticky,\n  stickyOffset = 0,\n}: RichTextEditorToolbarProps) => {\n  const { variant, editable } = useRichTextEditorContext();\n\n  if (!editable) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"rte-toolbar\",\n        variant !== \"default\" && `rte-toolbar--${variant}`,\n        sticky && \"sticky z-10\",\n        className\n      )}\n      data-sticky={sticky ? \"\" : undefined}\n      style={sticky ? { top: stickyOffset } : undefined}\n    >\n      {children}\n    </div>\n  );\n};\n",
      "path": "editor/rte-toolbar.tsx",
      "target": "@components/editor/rte-toolbar.tsx",
      "type": "registry:component"
    },
    {
      "content": "import { useEffect, useMemo, useRef, useState } from \"react\";\n\nimport { useRichTextEditorContext } from \"./rte-context\";\nimport type { RichTextEditorFooterProps } from \"./types\";\nimport { cn } from \"./ui/utils\";\n\nexport const Footer = ({\n  children,\n  className,\n  sticky = false,\n  stickyOffset = 0,\n  showWordCount = false,\n  wordCountClassName,\n  wordCountFormatter,\n}: RichTextEditorFooterProps) => {\n  const { editor, variant } = useRichTextEditorContext();\n  const [, forceUpdate] = useState(0);\n  const rafRef = useRef<number | null>(null);\n\n  useEffect(() => {\n    if (!editor || !showWordCount) {\n      return;\n    }\n\n    const scheduleUpdate = () => {\n      if (rafRef.current !== null) {\n        return;\n      }\n      rafRef.current = requestAnimationFrame(() => {\n        rafRef.current = null;\n        forceUpdate((t) => t + 1);\n      });\n    };\n\n    const handleTransaction = ({\n      transaction,\n    }: {\n      transaction: { docChanged: boolean };\n    }) => {\n      if (transaction.docChanged) {\n        scheduleUpdate();\n      }\n    };\n\n    editor.on(\"transaction\", handleTransaction);\n    scheduleUpdate();\n\n    return () => {\n      editor.off(\"transaction\", handleTransaction);\n      if (rafRef.current !== null) {\n        cancelAnimationFrame(rafRef.current);\n      }\n    };\n  }, [editor, showWordCount]);\n\n  const wordCount = useMemo(() => {\n    if (!editor || !showWordCount) {\n      return null;\n    }\n\n    const count = (editor.storage as unknown as Record<string, unknown>)\n      .characterCount as\n      | { words: () => number; characters: () => number }\n      | undefined;\n    const words = count?.words() ?? 0;\n    const characters = count?.characters() ?? 0;\n\n    return { characters, words };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [\n    editor,\n    showWordCount,\n    wordCountFormatter,\n    editor?.state.doc.content.size,\n  ]);\n\n  if (!showWordCount && !children) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"rte-footer\",\n        variant !== \"default\" && `rte-footer--${variant}`,\n        sticky && \"sticky z-10\",\n        className\n      )}\n      data-sticky={sticky ? \"\" : undefined}\n      style={sticky ? { bottom: stickyOffset } : undefined}\n    >\n      {showWordCount && wordCount && (\n        <span className={cn(\"rte-footer-word-count\", wordCountClassName)}>\n          {wordCountFormatter ? (\n            wordCountFormatter(wordCount)\n          ) : (\n            <>\n              <span className=\"rte-footer-word-count-stat\">\n                <span className=\"rte-footer-word-count-number\">\n                  {wordCount.words}\n                </span>\n                <span className=\"rte-footer-word-count-unit\">words</span>\n              </span>\n              <span\n                className=\"rte-footer-word-count-separator\"\n                aria-hidden=\"true\"\n              />\n              <span className=\"rte-footer-word-count-stat\">\n                <span className=\"rte-footer-word-count-number\">\n                  {wordCount.characters}\n                </span>\n                <span className=\"rte-footer-word-count-unit\">characters</span>\n              </span>\n            </>\n          )}\n        </span>\n      )}\n      {children}\n    </div>\n  );\n};\n",
      "path": "editor/rte-footer.tsx",
      "target": "@components/editor/rte-footer.tsx",
      "type": "registry:component"
    },
    {
      "content": "import { EditorContent } from \"@tiptap/react\";\n\nimport { useRichTextEditorContext } from \"./rte-context\";\nimport type { RichTextEditorContentProps } from \"./types\";\nimport { cn } from \"./ui/utils\";\n\nexport const Content = ({\n  children,\n  className,\n}: RichTextEditorContentProps) => {\n  const { editor } = useRichTextEditorContext();\n\n  return (\n    <div className=\"relative\">\n      <EditorContent\n        editor={editor}\n        className={cn(\"rte-content w-full\", className)}\n        spellCheck={false}\n      />\n      {children}\n    </div>\n  );\n};\n",
      "path": "editor/rte-content.tsx",
      "target": "@components/editor/rte-content.tsx",
      "type": "registry:component"
    },
    {
      "content": "import type { RichTextEditorControlsGroupProps } from \"./types\";\nimport { cn } from \"./ui/utils\";\n\nexport const ControlsGroup = ({\n  children,\n  className,\n}: RichTextEditorControlsGroupProps) => (\n  <div className={cn(\"rte-controls-group\", className)}>{children}</div>\n);\n",
      "path": "editor/rte-controls-group.tsx",
      "target": "@components/editor/rte-controls-group.tsx",
      "type": "registry:component"
    },
    {
      "content": "export interface RichTextEditorLabels {\n  boldControlLabel: string;\n  italicControlLabel: string;\n  underlineControlLabel: string;\n  strikeControlLabel: string;\n  clearFormattingControlLabel: string;\n  codeControlLabel: string;\n  codeBlockControlLabel: string;\n  h1ControlLabel: string;\n  h2ControlLabel: string;\n  h3ControlLabel: string;\n  h4ControlLabel: string;\n  h5ControlLabel: string;\n  h6ControlLabel: string;\n  bulletListControlLabel: string;\n  orderedListControlLabel: string;\n  blockquoteControlLabel: string;\n  hrControlLabel: string;\n  linkControlLabel: string;\n  unlinkControlLabel: string;\n  undoControlLabel: string;\n  redoControlLabel: string;\n  alignLeftControlLabel: string;\n  alignCenterControlLabel: string;\n  alignRightControlLabel: string;\n  alignJustifyControlLabel: string;\n  highlightControlLabel: string;\n  subscriptControlLabel: string;\n  superscriptControlLabel: string;\n  tasksControlLabel: string;\n  tasksSinkLabel: string;\n  tasksLiftLabel: string;\n  sourceCodeControlLabel: string;\n  // link editor\n  linkEditorInputLabel: string;\n  linkEditorInputPlaceholder: string;\n  linkEditorExternalLink: string;\n  linkEditorInternalLink: string;\n  linkEditorSave: string;\n}\n\nexport const DEFAULT_LABELS: RichTextEditorLabels = {\n  alignCenterControlLabel: \"Align center\",\n  alignJustifyControlLabel: \"Align justify\",\n  alignLeftControlLabel: \"Align left\",\n  alignRightControlLabel: \"Align right\",\n  blockquoteControlLabel: \"Blockquote\",\n  boldControlLabel: \"Bold\",\n  bulletListControlLabel: \"Bullet list\",\n  clearFormattingControlLabel: \"Clear formatting\",\n  codeBlockControlLabel: \"Code block\",\n  codeControlLabel: \"Code\",\n  h1ControlLabel: \"Heading 1\",\n  h2ControlLabel: \"Heading 2\",\n  h3ControlLabel: \"Heading 3\",\n  h4ControlLabel: \"Heading 4\",\n  h5ControlLabel: \"Heading 5\",\n  h6ControlLabel: \"Heading 6\",\n  highlightControlLabel: \"Highlight\",\n  hrControlLabel: \"Horizontal rule\",\n  italicControlLabel: \"Italic\",\n  linkControlLabel: \"Link\",\n  linkEditorExternalLink: \"Open in new tab\",\n  linkEditorInputLabel: \"Enter URL\",\n  linkEditorInputPlaceholder: \"https://example.com\",\n  linkEditorInternalLink: \"Open in same tab\",\n  linkEditorSave: \"Save\",\n  orderedListControlLabel: \"Ordered list\",\n  redoControlLabel: \"Redo\",\n  sourceCodeControlLabel: \"Source code\",\n  strikeControlLabel: \"Strikethrough\",\n  subscriptControlLabel: \"Subscript\",\n  superscriptControlLabel: \"Superscript\",\n  tasksControlLabel: \"Task list\",\n  tasksLiftLabel: \"Increase task level\",\n  tasksSinkLabel: \"Decrease task level\",\n  underlineControlLabel: \"Underline\",\n  undoControlLabel: \"Undo\",\n  unlinkControlLabel: \"Remove link\",\n};\n",
      "path": "editor/labels.ts",
      "target": "@components/editor/labels.ts",
      "type": "registry:component"
    },
    {
      "content": "import type React from \"react\";\n\nexport interface RichTextEditorIcons {\n  boldControlIcon: React.ReactNode;\n  italicControlIcon: React.ReactNode;\n  underlineControlIcon: React.ReactNode;\n  strikeControlIcon: React.ReactNode;\n  clearFormattingControlIcon: React.ReactNode;\n  codeControlIcon: React.ReactNode;\n  codeBlockControlIcon: React.ReactNode;\n  h1ControlIcon: React.ReactNode;\n  h2ControlIcon: React.ReactNode;\n  h3ControlIcon: React.ReactNode;\n  h4ControlIcon: React.ReactNode;\n  h5ControlIcon: React.ReactNode;\n  h6ControlIcon: React.ReactNode;\n  bulletListControlIcon: React.ReactNode;\n  orderedListControlIcon: React.ReactNode;\n  blockquoteControlIcon: React.ReactNode;\n  hrControlIcon: React.ReactNode;\n  linkControlIcon: React.ReactNode;\n  unlinkControlIcon: React.ReactNode;\n  undoControlIcon: React.ReactNode;\n  redoControlIcon: React.ReactNode;\n  alignLeftControlIcon: React.ReactNode;\n  alignCenterControlIcon: React.ReactNode;\n  alignRightControlIcon: React.ReactNode;\n  alignJustifyControlIcon: React.ReactNode;\n  highlightControlIcon: React.ReactNode;\n  subscriptControlIcon: React.ReactNode;\n  superscriptControlIcon: React.ReactNode;\n  languageIcons: Record<string, React.ReactNode>;\n}\n",
      "path": "editor/icon-types.ts",
      "target": "@components/editor/icon-types.ts",
      "type": "registry:component"
    },
    {
      "content": "import {\n  Bold,\n  Code,\n  Heading1,\n  Heading2,\n  Heading3,\n  Heading4,\n  Heading5,\n  Heading6,\n  Highlighter,\n  Italic,\n  Link,\n  Link2Off,\n  List,\n  ListOrdered,\n  Minus,\n  Redo2,\n  RemoveFormatting,\n  SquareCode,\n  Strikethrough,\n  Subscript,\n  Superscript,\n  TextAlignCenter,\n  TextAlignEnd,\n  TextAlignJustify,\n  TextAlignStart,\n  TextQuote,\n  Underline,\n  Undo2,\n} from \"lucide-react\";\nimport React from \"react\";\n\nimport type { RichTextEditorIcons } from \"./icon-types\";\nimport { DEFAULT_LANGUAGE_ICONS } from \"./language-icons\";\n\n// ---------------------------------------------------------------------------\n// Embed toolbar icons (used by ResizableNodeView / EmbedToolbar)\n// ---------------------------------------------------------------------------\n\ntype AlignDir = \"left\" | \"center\" | \"right\";\n\nexport const ALIGN_PATHS: Record<AlignDir, React.ReactNode> = {\n  center: (\n    <>\n      <line x1=\"21\" y1=\"6\" x2=\"3\" y2=\"6\" />\n      <line x1=\"17\" y1=\"12\" x2=\"7\" y2=\"12\" />\n      <line x1=\"19\" y1=\"18\" x2=\"5\" y2=\"18\" />\n    </>\n  ),\n  left: (\n    <>\n      <line x1=\"21\" y1=\"6\" x2=\"3\" y2=\"6\" />\n      <line x1=\"15\" y1=\"12\" x2=\"3\" y2=\"12\" />\n      <line x1=\"17\" y1=\"18\" x2=\"3\" y2=\"18\" />\n    </>\n  ),\n  right: (\n    <>\n      <line x1=\"21\" y1=\"6\" x2=\"3\" y2=\"6\" />\n      <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n      <line x1=\"21\" y1=\"18\" x2=\"7\" y2=\"18\" />\n    </>\n  ),\n};\n\nexport type { AlignDir };\n\nexport const AlignIcon = ({ dir }: { dir: AlignDir }) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"rte-editor-icon\"\n  >\n    {ALIGN_PATHS[dir]}\n  </svg>\n);\n\nexport const EditUrlIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"rte-editor-icon\"\n  >\n    <path d=\"M12 20h9\" />\n    <path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" />\n  </svg>\n);\n\nexport const OpenInYouTubeIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"rte-editor-icon\"\n  >\n    <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n    <polyline points=\"15 3 21 3 21 9\" />\n    <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n  </svg>\n);\n\nexport const DeleteEmbedIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"rte-editor-icon\"\n  >\n    <polyline points=\"3 6 5 6 21 6\" />\n    <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n  </svg>\n);\n\nconst iconProps = { className: \"rte-editor-icon\" };\n\nexport const DEFAULT_ICONS: RichTextEditorIcons = {\n  alignCenterControlIcon: <TextAlignCenter {...iconProps} />,\n  alignJustifyControlIcon: <TextAlignJustify {...iconProps} />,\n  alignLeftControlIcon: <TextAlignStart {...iconProps} />,\n  alignRightControlIcon: <TextAlignEnd {...iconProps} />,\n  blockquoteControlIcon: <TextQuote {...iconProps} />,\n  boldControlIcon: <Bold {...iconProps} />,\n  bulletListControlIcon: <List {...iconProps} />,\n  clearFormattingControlIcon: <RemoveFormatting {...iconProps} />,\n  codeBlockControlIcon: <SquareCode {...iconProps} />,\n  codeControlIcon: <Code {...iconProps} />,\n  h1ControlIcon: <Heading1 {...iconProps} />,\n  h2ControlIcon: <Heading2 {...iconProps} />,\n  h3ControlIcon: <Heading3 {...iconProps} />,\n  h4ControlIcon: <Heading4 {...iconProps} />,\n  h5ControlIcon: <Heading5 {...iconProps} />,\n  h6ControlIcon: <Heading6 {...iconProps} />,\n  highlightControlIcon: <Highlighter {...iconProps} />,\n  hrControlIcon: <Minus {...iconProps} />,\n  italicControlIcon: <Italic {...iconProps} />,\n  languageIcons: DEFAULT_LANGUAGE_ICONS,\n  linkControlIcon: <Link {...iconProps} />,\n  orderedListControlIcon: <ListOrdered {...iconProps} />,\n  redoControlIcon: <Redo2 {...iconProps} />,\n  strikeControlIcon: <Strikethrough {...iconProps} />,\n  subscriptControlIcon: <Subscript {...iconProps} />,\n  superscriptControlIcon: <Superscript {...iconProps} />,\n  underlineControlIcon: <Underline {...iconProps} />,\n  undoControlIcon: <Undo2 {...iconProps} />,\n  unlinkControlIcon: <Link2Off {...iconProps} />,\n};\n",
      "path": "editor/icons.tsx",
      "target": "@components/editor/icons.tsx",
      "type": "registry:component"
    },
    {
      "content": "import React from \"react\";\n\nconst LangIcon = ({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth={1.5}\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className={className}\n  >\n    {children}\n  </svg>\n);\n\nexport const DEFAULT_LANGUAGE_ICONS: Record<string, React.ReactNode> = {\n  bash: (\n    <LangIcon>\n      <rect x=\"2.5\" y=\"4\" width=\"19\" height=\"16\" rx=\"2\" />\n      <path d=\"M6.5 9.5l3.5 3-3.5 3\" />\n      <path d=\"M12 15.5h5.5\" />\n    </LangIcon>\n  ),\n  c: (\n    <LangIcon>\n      <circle cx=\"12\" cy=\"12\" r=\"8.5\" />\n      <path d=\"M15.6 9.3a4.5 4.5 0 1 0 0 5.4\" />\n    </LangIcon>\n  ),\n  cpp: (\n    <LangIcon>\n      <circle cx=\"9.5\" cy=\"12\" r=\"6.5\" />\n      <path d=\"M12.3 9.7a3.3 3.3 0 1 0 0 4.6\" />\n      <path d=\"M15.7 10.5v3M14.2 12h3\" />\n      <path d=\"M19.3 10.5v3M17.8 12h3\" />\n    </LangIcon>\n  ),\n  css: (\n    <LangIcon>\n      <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"4\" />\n      <path d=\"M9.7 7.5l-1 9\" />\n      <path d=\"M15.3 7.5l-1 9\" />\n      <path d=\"M7 10.3h11\" />\n      <path d=\"M6.5 14.3h11\" />\n    </LangIcon>\n  ),\n  go: (\n    <LangIcon>\n      <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"4\" />\n      <path d=\"M12.6 9.7A3.3 3.3 0 1 0 12.6 14.3\" />\n      <path d=\"M12.6 12h-1.9\" />\n      <circle cx=\"17\" cy=\"12\" r=\"2.9\" />\n    </LangIcon>\n  ),\n  html: (\n    <LangIcon>\n      <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"4\" />\n      <path d=\"M9.5 8.5L6.5 12l3 3.5\" />\n      <path d=\"M14.5 8.5L17.5 12l-3 3.5\" />\n      <path d=\"M13 7.3l-2 9.4\" />\n    </LangIcon>\n  ),\n  java: (\n    <LangIcon>\n      <path d=\"M6 10h10v5a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3v-5z\" />\n      <path d=\"M16 12h1.3a1.8 1.8 0 0 1 0 3.6H16\" />\n      <path d=\"M9.3 3.8c-.9.8-.9 1.6 0 2.4M13.3 3.8c-.9.8-.9 1.6 0 2.4\" />\n      <path d=\"M6 19.3h10\" />\n    </LangIcon>\n  ),\n  javascript: (\n    <LangIcon>\n      <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"4\" />\n      <path d=\"M10.5 8v6.5a2 2 0 0 1-3.5 1.3\" />\n      <path d=\"M17 8.8a2 2 0 0 0-2-.8c-1.1 0-2.2.6-2.2 1.7 0 1 .9 1.4 2.2 1.7 1.3.3 2.3.7 2.3 1.9 0 1.1-1 1.8-2.3 1.8a2.4 2.4 0 0 1-2.3-1.4\" />\n    </LangIcon>\n  ),\n  json: (\n    <LangIcon>\n      <path d=\"M8 3H7a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2 2 2 0 0 1 2 2v4a2 2 0 0 0 2 2\" />\n      <path d=\"M16 3h1a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2 2 2 0 0 0-2 2v4a2 2 0 0 1-2 2\" />\n    </LangIcon>\n  ),\n  kotlin: (\n    <LangIcon>\n      <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"4\" />\n      <path d=\"M9 7.5v9\" />\n      <path d=\"M9 12l6-4.5\" />\n      <path d=\"M9 12l6 4.5\" />\n    </LangIcon>\n  ),\n  markdown: (\n    <LangIcon>\n      <rect x=\"2\" y=\"5\" width=\"20\" height=\"14\" rx=\"2\" />\n      <path d=\"M6 15V9l3 4 3-4v6\" />\n      <path d=\"M16.5 9v6\" />\n      <path d=\"M14.5 13l2 2 2-2\" />\n    </LangIcon>\n  ),\n  php: (\n    <LangIcon>\n      <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"4\" />\n      <path d=\"M11.5 8.5L7.5 12l4 3.5\" />\n      <path d=\"M15.2 8.2c1.1 0 1.9.6 1.9 1.6 0 1.1-.9 1.5-1.8 1.8-.6.2-1 .5-1 1.1\" />\n      <circle cx=\"14.3\" cy=\"16.2\" r=\".5\" fill=\"currentColor\" stroke=\"none\" />\n    </LangIcon>\n  ),\n  plaintext: (\n    <LangIcon>\n      <rect x=\"4\" y=\"2.5\" width=\"16\" height=\"19\" rx=\"2\" />\n      <line x1=\"7.5\" y1=\"7.5\" x2=\"16.5\" y2=\"7.5\" />\n      <line x1=\"7.5\" y1=\"11.5\" x2=\"16.5\" y2=\"11.5\" />\n      <line x1=\"7.5\" y1=\"15.5\" x2=\"13.5\" y2=\"15.5\" />\n    </LangIcon>\n  ),\n  python: (\n    <LangIcon>\n      <path d=\"M4.5 15c0-3 2.5-4.7 5.7-4.7h1.6c2.8 0 5-2 5-4.5\" />\n      <path d=\"M9.5 3.3c1.3-.3 2.6 0 3.3 1-.7 1-2 1.3-3.3 1\" />\n      <circle cx=\"15.5\" cy=\"4.3\" r=\".55\" fill=\"currentColor\" stroke=\"none\" />\n      <path d=\"M17.8 4.5l1.4-.6M17.8 4.9l1.4.6\" />\n    </LangIcon>\n  ),\n  ruby: (\n    <LangIcon>\n      <polygon points=\"12 2.5 19.5 8 12 21.5 4.5 8\" />\n      <path d=\"M4.5 8h15\" />\n      <path d=\"M12 2.5v19\" />\n    </LangIcon>\n  ),\n  rust: (\n    <LangIcon>\n      <circle cx=\"12\" cy=\"12\" r=\"7\" />\n      <circle cx=\"12\" cy=\"12\" r=\"2.3\" />\n      <path d=\"M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3\" />\n      <path d=\"M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1\" />\n    </LangIcon>\n  ),\n  sql: (\n    <LangIcon>\n      <ellipse cx=\"12\" cy=\"6\" rx=\"8\" ry=\"3\" />\n      <path d=\"M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6\" />\n      <path d=\"M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6\" />\n    </LangIcon>\n  ),\n  swift: (\n    <LangIcon>\n      <path d=\"M19 4.5c-2.6 4-5.5 7-8.3 9 2-.2 3.7-1 4.8-2.6-.7 2.8-2.8 5-5.3 6.1 3 .5 5.6-.5 7.2-2.4.4 2-.4 3.9-1.9 4.9 3.7-1.3 6-4.2 6.6-7.6-1 2.6-2.8 4.6-4.6 5.8\" />\n    </LangIcon>\n  ),\n  typescript: (\n    <LangIcon>\n      <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"4\" />\n      <path d=\"M7.3 8.2h5M9.8 8.2v7.6\" />\n      <path d=\"M17 8.8a2 2 0 0 0-2-.8c-1.1 0-2.2.6-2.2 1.7 0 1 .9 1.4 2.2 1.7 1.3.3 2.3.7 2.3 1.9 0 1.1-1 1.8-2.3 1.8a2.4 2.4 0 0 1-2.3-1.4\" />\n    </LangIcon>\n  ),\n  xml: (\n    <LangIcon>\n      <path d=\"M8 7l-4.5 5 4.5 5\" />\n      <path d=\"M16 7l4.5 5-4.5 5\" />\n      <path d=\"M12 8v8\" />\n    </LangIcon>\n  ),\n  yaml: (\n    <LangIcon>\n      <rect x=\"2.5\" y=\"3\" width=\"19\" height=\"18\" rx=\"2\" />\n      <path d=\"M6.5 8h2M9.5 8h8\" />\n      <path d=\"M6.5 12h2M9.5 12h6\" />\n      <path d=\"M6.5 16h2M9.5 16h7\" />\n    </LangIcon>\n  ),\n};\n",
      "path": "editor/language-icons.tsx",
      "target": "@components/editor/language-icons.tsx",
      "type": "registry:component"
    },
    {
      "content": "import type { Editor } from \"@tiptap/react\";\n\nimport type { RichTextEditorIcons } from \"./icon-types\";\nimport type { RichTextEditorLabels } from \"./labels\";\n\nexport type RichTextEditorVariant = \"default\" | \"subtle\" | \"compact\";\n\nexport interface RichTextEditorProps {\n  editor: Editor | null;\n  children: React.ReactNode;\n  className?: string;\n  labels?: Partial<RichTextEditorLabels>;\n  icons?: Partial<RichTextEditorIcons>;\n  variant?: RichTextEditorVariant;\n  editable?: boolean;\n}\n\nexport interface RichTextEditorToolbarProps {\n  children: React.ReactNode;\n  className?: string;\n  sticky?: boolean;\n  stickyOffset?: number | string;\n}\n\nexport interface RichTextEditorContentProps {\n  children?: React.ReactNode;\n  className?: string;\n}\n\nexport interface RichTextEditorControlsGroupProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\nexport interface RichTextEditorControlProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  active?: boolean;\n  interactive?: boolean;\n}\n\nexport interface WordCountInfo {\n  words: number;\n  characters: number;\n}\n\nexport interface RichTextEditorFooterProps {\n  children?: React.ReactNode;\n  className?: string;\n  sticky?: boolean;\n  stickyOffset?: number | string;\n  showWordCount?: boolean;\n  wordCountClassName?: string;\n  wordCountFormatter?: (info: WordCountInfo) => string;\n}\n",
      "path": "editor/types.ts",
      "target": "@components/editor/types.ts",
      "type": "registry:component"
    },
    {
      "content": "/* ── Main Editor Wrapper ── */\n.rte-root {\n  position: relative;\n  border-radius: var(--radius);\n  border: 1px solid var(--border);\n  background-color: var(--background);\n  transition:\n    border-color 150ms ease-out,\n    box-shadow 150ms ease-out;\n}\n\n.rte-editor-icon {\n  width: 1rem;\n  height: 1rem;\n  flex-shrink: 0;\n}\n\nbutton.rte-control-button {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  height: 2rem;\n  min-width: 2rem;\n  padding: 0;\n  transition:\n    background-color 150ms ease-out,\n    color 150ms ease-out,\n    transform 100ms ease-out;\n}\n\nbutton.rte-control-button:active {\n  transform: scale(0.96);\n}\n\n/* ── Editor Content ── */\n.rte-content .ProseMirror {\n  outline: 0;\n  padding: var(--spacing, 0.75rem);\n  min-height: 140px;\n}\n\n.rte-content .ProseMirror > *:last-child {\n  margin-bottom: 0;\n}\n\n.rte-content .ProseMirror p.is-editor-empty:first-of-type::before {\n  content: attr(data-placeholder);\n  pointer-events: none;\n  user-select: none;\n  float: left;\n  height: 0;\n  color: var(--muted-foreground);\n  transition: opacity 150ms ease-out;\n}\n\n.rte-content .ProseMirror p {\n  margin-top: 0;\n  margin-bottom: 0.75rem;\n  line-height: 1.75;\n}\n\n.rte-content .ProseMirror h1,\n.rte-content .ProseMirror h2,\n.rte-content .ProseMirror h3,\n.rte-content .ProseMirror h4,\n.rte-content .ProseMirror h5,\n.rte-content .ProseMirror h6,\n.rte-content .ProseMirror p {\n  margin-top: 0;\n}\n\n.rte-content .ProseMirror h1 {\n  font-size: 1.5rem;\n  font-weight: 700;\n  letter-spacing: -0.025em;\n  margin-bottom: 1rem;\n  margin-top: 1.5rem;\n}\n.rte-content .ProseMirror h1:first-child {\n  margin-top: 0;\n}\n\n.rte-content .ProseMirror h2 {\n  font-size: 1.25rem;\n  font-weight: 600;\n  letter-spacing: -0.025em;\n  margin-bottom: 0.75rem;\n  margin-top: 1.25rem;\n}\n.rte-content .ProseMirror h2:first-child {\n  margin-top: 0;\n}\n\n.rte-content .ProseMirror h3 {\n  font-size: 1.125rem;\n  font-weight: 600;\n  margin-bottom: 0.5rem;\n  margin-top: 1rem;\n}\n.rte-content .ProseMirror h3:first-child {\n  margin-top: 0;\n}\n\n.rte-content .ProseMirror h4 {\n  font-size: 1rem;\n  font-weight: 600;\n  margin-bottom: 0.5rem;\n  margin-top: 0.75rem;\n}\n.rte-content .ProseMirror h4:first-child {\n  margin-top: 0;\n}\n\n.rte-content .ProseMirror h5 {\n  font-size: 0.875rem;\n  font-weight: 600;\n  margin-bottom: 0.5rem;\n  margin-top: 0.75rem;\n}\n.rte-content .ProseMirror h5:first-child {\n  margin-top: 0;\n}\n\n.rte-content .ProseMirror h6 {\n  font-size: 0.875rem;\n  font-weight: 500;\n  margin-bottom: 0.5rem;\n  margin-top: 0.75rem;\n}\n.rte-content .ProseMirror h6:first-child {\n  margin-top: 0;\n}\n\n.rte-content .ProseMirror ul {\n  list-style-type: disc;\n  padding-left: 1.5rem;\n  margin-bottom: 0.75rem;\n}\n\n.rte-content .ProseMirror ol {\n  list-style-type: decimal;\n  padding-left: 1.5rem;\n  margin-bottom: 0.75rem;\n}\n\n.rte-content .ProseMirror li {\n  line-height: 1.75;\n}\n\n.rte-content .ProseMirror li > p {\n  margin-bottom: 0;\n}\n\n.rte-content .ProseMirror ul li,\n.rte-content .ProseMirror ol li {\n  margin-top: 2px;\n}\n\n.rte-content .ProseMirror blockquote {\n  border-left: 3px solid var(--border);\n  padding-left: 1rem;\n  margin-top: 1rem;\n  margin-bottom: 1rem;\n  color: var(--muted-foreground);\n  font-style: italic;\n}\n\n.rte-content .ProseMirror :where(pre) {\n  background: #1e1e2e;\n  color: #cdd6f4;\n  border-radius: var(--radius);\n  border: 1px solid var(--border);\n  padding: 1rem;\n  margin-top: 1rem;\n  margin-bottom: 1rem;\n  overflow-x: auto;\n  font-family: var(--font-mono);\n  font-size: 0.875rem;\n}\n\n.rte-content .ProseMirror :where(pre) code {\n  background: none;\n  color: inherit;\n  border: none;\n  font-size: 0.875rem;\n  font-weight: 400;\n  padding: 0;\n}\n\n.rte-content .ProseMirror hr {\n  border: none;\n  border-top: 1px solid var(--border);\n  margin-top: 1.5rem;\n  margin-bottom: 1.5rem;\n}\n\n.rte-content .ProseMirror a {\n  color: var(--primary);\n  text-decoration: underline;\n  text-underline-offset: 4px;\n  cursor: pointer;\n  transition:\n    color 150ms ease-out,\n    text-decoration-color 150ms ease-out;\n  text-decoration-color: color-mix(in srgb, var(--primary) 40%, transparent);\n}\n\n.rte-content .ProseMirror a:hover {\n  color: color-mix(in srgb, var(--primary) 80%, transparent);\n  text-decoration-color: color-mix(in srgb, var(--primary) 80%, transparent);\n}\n\n.rte-content .ProseMirror code {\n  font-size: 0.875em;\n  font-weight: 500;\n  background: color-mix(in srgb, var(--muted) 70%, transparent);\n  padding: 0.125rem 0.375rem;\n  border-radius: calc(var(--radius) - 4px);\n  transition: background-color 150ms ease-out;\n}\n\n/* ── Toolbars and Footer ── */\n.rte-toolbar {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-wrap: wrap;\n  gap: 0.5rem;\n  top: var(--rte-sticky-offset, 0px);\n  background-color: color-mix(in srgb, var(--muted) 35%, var(--background));\n  z-index: 1;\n  border-start-end-radius: var(--radius);\n  border-start-start-radius: var(--radius);\n  border-bottom: 1px solid var(--border);\n  padding: 0.5rem 0.75rem;\n  transition:\n    box-shadow 150ms ease-out,\n    background-color 150ms ease-out;\n}\n\n.rte-toolbar[data-sticky] {\n  position: sticky;\n  background-color: color-mix(in srgb, var(--background) 85%, transparent);\n  backdrop-filter: blur(8px);\n  box-shadow:\n    0px 0px 0px 1px oklch(0 0 0 / 0.04),\n    0px 4px 12px 0px oklch(0 0 0 / 0.05);\n}\n\n@media (prefers-color-scheme: dark) {\n  .rte-toolbar[data-sticky] {\n    box-shadow:\n      0px 0px 0px 1px oklch(1 0 0 / 0.06),\n      0px 4px 12px 0px oklch(0 0 0 / 0.3);\n  }\n}\n\n.rte-footer {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  flex-wrap: wrap;\n  gap: 0.5rem;\n  background-color: var(--background);\n  z-index: 1;\n  border-end-end-radius: var(--radius);\n  border-end-start-radius: var(--radius);\n  border-top: 1px solid var(--border);\n  padding: 0.5rem 0.75rem;\n  font-size: 0.75rem;\n  color: var(--muted-foreground);\n}\n\n.rte-footer[data-sticky] {\n  position: sticky;\n  bottom: 0;\n  background-color: color-mix(in srgb, var(--background) 85%, transparent);\n  backdrop-filter: blur(8px);\n  box-shadow:\n    0px 0px 0px 1px oklch(0 0 0 / 0.04),\n    0px -4px 12px 0px oklch(0 0 0 / 0.05);\n}\n\n@media (prefers-color-scheme: dark) {\n  .rte-footer[data-sticky] {\n    box-shadow:\n      0px 0px 0px 1px oklch(1 0 0 / 0.06),\n      0px -4px 12px 0px oklch(0 0 0 / 0.3);\n  }\n}\n\n.rte-footer-word-count {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.375rem;\n  white-space: nowrap;\n  font-size: 0.75rem;\n  letter-spacing: 0.01em;\n}\n\n.rte-footer-word-count-stat {\n  display: inline-flex;\n  align-items: baseline;\n  gap: 0.25rem;\n}\n\n.rte-footer-word-count-number {\n  font-weight: 600;\n  font-variant-numeric: tabular-nums;\n  color: var(--foreground);\n}\n\n.rte-footer-word-count-unit {\n  color: var(--muted-foreground);\n}\n\n.rte-footer-word-count-separator {\n  width: 3px;\n  height: 3px;\n  flex-shrink: 0;\n  border-radius: 9999px;\n  background-color: color-mix(\n    in srgb,\n    var(--muted-foreground) 45%,\n    transparent\n  );\n}\n\n.rte-toolbar--subtle + .rte-content + .rte-footer {\n  background-color: var(--muted);\n}\n.rte-toolbar--compact + .rte-content + .rte-footer {\n  background-color: var(--muted);\n}\n\n@media (max-width: 640px) {\n  .rte-toolbar {\n    flex-wrap: nowrap;\n    overflow-x: auto;\n    justify-content: flex-start;\n    scrollbar-width: none;\n    -ms-overflow-style: none;\n    -webkit-overflow-scrolling: touch;\n    --rte-toolbar-fade-end: 1.5rem;\n    mask-image: linear-gradient(\n      to right,\n      transparent,\n      black var(--rte-toolbar-fade-start),\n      black calc(100% - var(--rte-toolbar-fade-end)),\n      transparent\n    );\n  }\n  .rte-toolbar::-webkit-scrollbar {\n    display: none;\n  }\n  @supports (animation-timeline: scroll()) {\n    .rte-toolbar {\n      --rte-toolbar-fade-end: 0px;\n      animation: rte-toolbar-fade linear both;\n      animation-timeline: scroll(self inline);\n    }\n  }\n}\n\n@property --rte-toolbar-fade-start {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n\n@property --rte-toolbar-fade-end {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n\n@keyframes rte-toolbar-fade {\n  from {\n    --rte-toolbar-fade-start: 0px;\n    --rte-toolbar-fade-end: 1.5rem;\n  }\n  10%,\n  90% {\n    --rte-toolbar-fade-start: 1.5rem;\n    --rte-toolbar-fade-end: 1.5rem;\n  }\n  to {\n    --rte-toolbar-fade-start: 1.5rem;\n    --rte-toolbar-fade-end: 0px;\n  }\n}\n\n.rte-root[data-variant=\"default\"] {\n  box-shadow: 0 1px 2px oklch(0 0 0 / 0.04);\n}\n.rte-root[data-variant=\"default\"] .rte-toolbar {\n  gap: 0.25rem;\n  padding: 0.375rem 0.5rem;\n  background-color: var(--background);\n}\n.rte-root[data-variant=\"default\"] .rte-content .ProseMirror {\n  padding: 1.25rem 1.5rem;\n}\n.rte-root[data-variant=\"default\"] .rte-controls-group {\n  display: flex;\n  align-items: center;\n  gap: 0.125rem;\n}\n.rte-root[data-variant=\"default\"]\n  .rte-controls-group\n  + .rte-controls-group::before {\n  content: \"\";\n  width: 1px;\n  height: 1.25rem;\n  margin-inline: 0.25rem;\n  background-color: var(--border);\n  flex-shrink: 0;\n}\n.rte-root[data-variant=\"default\"] .rte-controls-group > button {\n  border: none;\n  border-radius: calc(var(--radius) - 2px);\n  min-width: 2rem;\n  height: 2rem;\n  background-color: transparent;\n  color: var(--foreground);\n  transition:\n    background-color 150ms ease-out,\n    color 150ms ease-out;\n}\n.rte-root[data-variant=\"default\"]\n  .rte-controls-group\n  > button:hover:not(:disabled) {\n  background-color: var(--muted);\n}\n.rte-root[data-variant=\"default\"] .rte-controls-group > button:disabled {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n.rte-root[data-variant=\"default\"] .rte-controls-group > button:focus-visible {\n  outline: 2px solid color-mix(in srgb, var(--primary) 40%, transparent);\n  outline-offset: 1px;\n}\n.rte-root[data-variant=\"default\"] .rte-controls-group > button[data-state=\"on\"],\n.rte-root[data-variant=\"default\"]\n  .rte-controls-group\n  > button[aria-pressed=\"true\"] {\n  background-color: var(--accent);\n  color: var(--accent-foreground);\n}\n\n/* ── Subtle variant: elevated card with soft toolbar ── */\n.rte-root[data-variant=\"subtle\"] {\n  border-color: transparent;\n  box-shadow:\n    0 1px 3px rgba(0, 0, 0, 0.06),\n    0 1px 2px rgba(0, 0, 0, 0.04);\n}\n.rte-toolbar--subtle {\n  border-bottom: 1px solid var(--border);\n  padding: 0.375rem 0.5rem;\n  gap: 0.375rem;\n  background-color: var(--muted);\n}\n.rte-root[data-variant=\"subtle\"] .rte-content .ProseMirror {\n  padding: 0.875rem;\n}\n.rte-root[data-variant=\"subtle\"] .rte-controls-group {\n  display: flex;\n  align-items: center;\n}\n.rte-root[data-variant=\"subtle\"]\n  .rte-controls-group\n  + .rte-controls-group::before {\n  content: \"\";\n  width: 1px;\n  height: 1rem;\n  margin-inline: 0.25rem;\n  background-color: var(--border);\n  flex-shrink: 0;\n}\n.rte-root[data-variant=\"subtle\"] .rte-controls-group > button {\n  border: none;\n  border-radius: calc(var(--radius) - 2px);\n  min-width: 2rem;\n  height: 2rem;\n  background-color: transparent;\n  color: var(--muted-foreground);\n  transition:\n    background-color 150ms ease-out,\n    color 150ms ease-out;\n}\n.rte-root[data-variant=\"subtle\"] .rte-controls-group > button:hover {\n  background-color: var(--accent);\n  color: var(--accent-foreground);\n}\n.rte-root[data-variant=\"subtle\"] .rte-controls-group > button:focus-visible {\n  outline: 2px solid color-mix(in srgb, var(--primary) 40%, transparent);\n  outline-offset: 1px;\n  background-color: var(--accent);\n  color: var(--accent-foreground);\n}\n.rte-root[data-variant=\"subtle\"] .rte-controls-group > button[data-state=\"on\"],\n.rte-root[data-variant=\"subtle\"]\n  .rte-controls-group\n  > button[aria-pressed=\"true\"] {\n  background-color: color-mix(in srgb, var(--primary) 12%, transparent);\n  color: var(--primary);\n  box-shadow: inset 0 0 0 1px\n    color-mix(in srgb, var(--primary) 25%, transparent);\n}\n\n/* ── Compact variant: inline fused toolbar-content ── */\n.rte-root[data-variant=\"compact\"] {\n  border-radius: var(--radius);\n}\n.rte-toolbar--compact {\n  padding: 0.25rem 0.375rem;\n  gap: 0.25rem;\n  border-bottom: none;\n  background-color: var(--muted);\n}\n.rte-root[data-variant=\"compact\"] .rte-content .ProseMirror {\n  padding: 0.5rem 0.625rem;\n  min-height: 100px;\n}\n.rte-root[data-variant=\"compact\"] .rte-controls-group {\n  display: flex;\n  align-items: center;\n}\n.rte-root[data-variant=\"compact\"]\n  .rte-controls-group\n  + .rte-controls-group::before {\n  content: \"\";\n  width: 1px;\n  height: 0.875rem;\n  margin-inline: 0.125rem;\n  background-color: var(--border);\n  flex-shrink: 0;\n}\n.rte-root[data-variant=\"compact\"] .rte-controls-group > button {\n  border: none;\n  border-radius: calc(var(--radius) - 3px);\n  min-width: 1.5rem;\n  height: 1.5rem;\n  background-color: transparent;\n  color: var(--muted-foreground);\n  transition:\n    background-color 150ms ease-out,\n    color 150ms ease-out;\n}\n.rte-root[data-variant=\"compact\"] .rte-controls-group > button:hover {\n  background-color: var(--accent);\n  color: var(--accent-foreground);\n}\n.rte-root[data-variant=\"compact\"] .rte-controls-group > button:focus-visible {\n  outline: 2px solid color-mix(in srgb, var(--primary) 40%, transparent);\n  outline-offset: 1px;\n  background-color: var(--accent);\n  color: var(--accent-foreground);\n}\n.rte-root[data-variant=\"compact\"] .rte-controls-group > button[data-state=\"on\"],\n.rte-root[data-variant=\"compact\"]\n  .rte-controls-group\n  > button[aria-pressed=\"true\"] {\n  background-color: color-mix(in srgb, var(--primary) 10%, transparent);\n  color: var(--primary);\n  box-shadow: inset 0 0 0 1px\n    color-mix(in srgb, var(--primary) 25%, transparent);\n}\n.rte-root[data-variant=\"compact\"] .rte-editor-icon {\n  height: 0.875rem;\n  width: 0.875rem;\n}\n\n/* ── Controls group base (no separators) ── */\n.rte-controls-group {\n  display: flex;\n  align-items: center;\n}\n\n.rte-link-editor {\n  display: flex;\n  align-items: center;\n  gap: 0.25rem;\n}\n\n.rte-link-editor-input {\n  flex: 1;\n  min-width: 200px;\n}\n\n.rte-link-editor-save {\n  border-start-start-radius: 0;\n  border-end-start-radius: 0;\n}\n\n/* ── Task Lists ── */\n.rte-task-list {\n  list-style-type: none;\n  padding: 0;\n  padding-inline-start: 0;\n}\n\n.rte-task-list li {\n  margin: 0;\n  padding: 0;\n  display: flex;\n  align-items: flex-start;\n  gap: 0.5rem;\n  transition: opacity 150ms ease-out;\n}\n\n.rte-task-list li[data-checked=\"true\"] {\n  opacity: 0.6;\n}\n\n.rte-task-list li > div {\n  flex: 1;\n  min-width: 0;\n}\n\n.rte-task-list ul {\n  margin-top: 5px;\n}\n\n.rte-task-list p {\n  margin: 0;\n  padding: 0;\n}\n\n.rte-task-list label {\n  display: inline-flex;\n  align-items: center;\n  height: 1.75em;\n  cursor: pointer;\n}\n\n.rte-task-list input[type=\"checkbox\"] {\n  cursor: pointer;\n  appearance: none;\n  width: 18px;\n  height: 18px;\n  border: 1px solid var(--border);\n  border-radius: calc(var(--radius) - 2px);\n  background-color: var(--background);\n  vertical-align: middle;\n  position: relative;\n  flex-shrink: 0;\n  transition:\n    background-color 150ms ease-out,\n    border-color 150ms ease-out,\n    transform 100ms ease-out;\n}\n\n.rte-task-list input[type=\"checkbox\"]:active {\n  transform: scale(0.96);\n}\n\n.rte-task-list input[type=\"checkbox\"]:checked {\n  background-color: var(--primary);\n  border-color: var(--primary);\n}\n\n@keyframes checkmark-in {\n  0% {\n    transform: scale(0.5) rotate(-15deg);\n    opacity: 0;\n  }\n  100% {\n    transform: scale(1) rotate(0);\n    opacity: 1;\n  }\n}\n\n.rte-task-list input[type=\"checkbox\"]:checked::before {\n  position: absolute;\n  content: \"\";\n  inset: 0;\n  background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAxMCA3Ij48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNCA0LjU4NkwxLjcwNyAyLjI5M0ExIDEgMCAxMC4yOTMgMy43MDdsMyAzYS45OTcuOTk3IDAgMDAxLjQxNCAwbDUtNUExIDEgMCAxMDguMjkzLjI5M0w0IDQuNTg2eiIvPjwvc3ZnPg==\");\n  background-repeat: no-repeat;\n  background-size: 10px;\n  background-position: center;\n  animation: checkmark-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;\n}\n\n/* ── Embed nodes ── */\n.rte-embed-wrapper {\n  display: flex;\n  width: 100%;\n  margin: 1rem 0;\n  position: relative;\n  transition:\n    opacity 150ms ease-out,\n    transform 150ms ease-out;\n}\n\n.rte-embed-wrapper[data-align=\"left\"] {\n  justify-content: flex-start;\n  text-align: left;\n}\n.rte-embed-wrapper[data-align=\"center\"] {\n  justify-content: center;\n  text-align: center;\n}\n.rte-embed-wrapper[data-align=\"right\"] {\n  justify-content: flex-end;\n  text-align: right;\n}\n\n.rte-embed-wrapper[data-selected] .rte-embed {\n  outline: 2px solid var(--primary);\n  outline-offset: 2px;\n  border-radius: var(--radius);\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n}\n\n.rte-embed {\n  position: relative;\n  overflow: visible;\n  border-radius: var(--radius);\n  background: var(--muted);\n  display: inline-block;\n  transition:\n    outline-color 150ms ease-out,\n    box-shadow 150ms ease-out;\n}\n\n.rte-embed-iframe {\n  width: 100%;\n  height: 100%;\n  aspect-ratio: 16 / 9;\n  border: 0;\n  display: block;\n  border-radius: var(--radius);\n}\n\n.rte-embed-loading {\n  padding: 2rem;\n  text-align: center;\n  color: var(--muted-foreground);\n  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n@keyframes pulse {\n  0%,\n  100% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0.5;\n  }\n}\n\n/* ── Floating Embed Selector Toolbar ── */\n.rte-embed-selector {\n  position: absolute;\n  top: -46px;\n  left: 50%;\n  transform: translateX(-50%);\n  z-index: 50;\n  display: flex;\n  align-items: center;\n  height: 2.25rem;\n  background: var(--popover);\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.14);\n  padding: 0 0.375rem;\n  gap: 0.25rem;\n  white-space: nowrap;\n  animation: rte-bubble-pop-in 0.2s cubic-bezier(0.22, 1, 0.36, 1) forwards;\n  pointer-events: auto;\n}\n\n.rte-embed-selector-group {\n  display: flex;\n  align-items: center;\n  gap: 2px;\n}\n\n.rte-embed-selector-divider {\n  width: 1px;\n  height: 1.25rem;\n  background: var(--border);\n  margin: 0 0.125rem;\n  flex-shrink: 0;\n}\n\n.rte-embed-selector-btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  height: 1.75rem;\n  min-width: 1.75rem;\n  padding: 0 0.375rem;\n  border: none;\n  border-radius: calc(var(--radius) - 4px);\n  background: transparent;\n  color: var(--popover-foreground);\n  cursor: pointer;\n  font-size: 0.75rem;\n  font-weight: 500;\n  transition:\n    background-color 150ms ease-out,\n    color 150ms ease-out,\n    transform 100ms ease-out;\n}\n\n.rte-embed-selector-btn:hover {\n  background: var(--accent);\n  color: var(--accent-foreground);\n}\n\n.rte-embed-selector-btn:active {\n  transform: scale(0.96);\n}\n\n.rte-embed-selector-btn--active {\n  color: var(--primary);\n  background: color-mix(in srgb, var(--primary) 12%, transparent);\n}\n\n.rte-embed-selector-btn--preset {\n  font-weight: 600;\n  font-size: 0.7rem;\n  letter-spacing: 0.02em;\n}\n\n.rte-embed-selector-btn--danger:hover {\n  background: color-mix(in srgb, var(--destructive) 15%, transparent);\n  color: var(--destructive);\n}\n\n.rte-embed-selector-btn--save {\n  background: var(--primary);\n  color: var(--primary-foreground);\n  padding: 0 0.5rem;\n}\n.rte-embed-selector-btn--save:hover {\n  background: color-mix(in srgb, var(--primary) 90%, black);\n  color: var(--primary-foreground);\n}\n\n.rte-embed-selector-edit-form {\n  display: flex;\n  align-items: center;\n  gap: 0.375rem;\n}\n\n.rte-embed-selector-input {\n  height: 1.75rem;\n  width: 220px;\n  padding: 0 0.5rem;\n  font-size: 0.75rem;\n  border-radius: calc(var(--radius) - 4px);\n  border: 1px solid var(--border);\n  background: var(--background);\n  color: var(--foreground);\n  outline: none;\n}\n\n.rte-embed-selector-input:focus {\n  border-color: var(--primary);\n}\n\n.rte-embed-resize-badge {\n  position: absolute;\n  bottom: 8px;\n  right: 8px;\n  z-index: 40;\n  background: rgba(0, 0, 0, 0.75);\n  color: #fff;\n  font-size: 0.7rem;\n  font-weight: 600;\n  padding: 2px 6px;\n  border-radius: 4px;\n  pointer-events: none;\n  backdrop-filter: blur(4px);\n  font-family: var(--font-mono, monospace);\n}\n\n/* ── Resize handles for embeds ── */\n.rte-resize-handle {\n  position: absolute;\n  z-index: 10;\n  opacity: 0;\n  transition:\n    opacity 150ms ease-out,\n    background-color 150ms ease-out;\n}\n\n.rte-embed-wrapper[data-selected] .rte-resize-handle {\n  opacity: 1;\n}\n\n.rte-resize-handle--left {\n  top: 0;\n  bottom: 0;\n  left: -3px;\n  width: 6px;\n  cursor: ew-resize;\n  background: transparent;\n}\n\n.rte-resize-handle--left::after {\n  content: \"\";\n  position: absolute;\n  inset: 4px 1px;\n  width: 2px;\n  background: var(--primary);\n  border-radius: 1px;\n  transition:\n    width 150ms ease-out,\n    inset 150ms ease-out;\n}\n\n.rte-resize-handle--left:hover::after {\n  width: 4px;\n  inset: 4px 0;\n}\n\n.rte-resize-handle--right {\n  top: 0;\n  bottom: 0;\n  right: -3px;\n  width: 6px;\n  cursor: ew-resize;\n  background: transparent;\n}\n\n.rte-resize-handle--right::after {\n  content: \"\";\n  position: absolute;\n  inset: 4px 1px;\n  width: 2px;\n  background: var(--primary);\n  border-radius: 1px;\n  transition:\n    width 150ms ease-out,\n    inset 150ms ease-out;\n}\n\n.rte-resize-handle--right:hover::after {\n  width: 4px;\n  inset: 4px 0;\n}\n\n.rte-resize-handle--bottom {\n  left: 0;\n  right: 0;\n  bottom: -3px;\n  height: 6px;\n  cursor: ns-resize;\n  background: transparent;\n}\n\n.rte-resize-handle--bottom::after {\n  content: \"\";\n  position: absolute;\n  left: 4px;\n  right: 4px;\n  height: 2px;\n  background: var(--primary);\n  border-radius: 1px;\n  bottom: 1px;\n  transition:\n    height 150ms ease-out,\n    bottom 150ms ease-out;\n}\n\n.rte-resize-handle--bottom:hover::after {\n  height: 4px;\n  bottom: 0;\n}\n\n.rte-resize-handle--corner {\n  width: 12px;\n  height: 12px;\n  background: var(--primary);\n  border: 2px solid var(--background);\n  border-radius: 2px;\n  transition: transform 150ms ease-out;\n}\n\n.rte-resize-handle--corner-br {\n  bottom: -5px;\n  right: -5px;\n  cursor: nwse-resize;\n}\n\n.rte-resize-handle--corner-bl {\n  bottom: -5px;\n  left: -5px;\n  cursor: nesw-resize;\n}\n\n.rte-resize-handle--corner:hover {\n  transform: scale(1.15);\n}\n\n/* ── Bubble Menu ── */\n@keyframes rte-bubble-pop-in {\n  0% {\n    opacity: 0;\n    transform: scale(0.98) translateY(6px);\n  }\n  100% {\n    opacity: 1;\n    transform: scale(1) translateY(0);\n  }\n}\n\n.rte-bubble-menu {\n  display: flex;\n  align-items: center;\n  height: 2.25rem;\n  background: var(--popover);\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);\n  padding: 0 0.25rem;\n  animation: rte-bubble-pop-in 0.25s cubic-bezier(0.22, 1, 0.36, 1) forwards;\n}\n\n.rte-bubble-group {\n  display: flex;\n  align-items: center;\n  gap: 1px;\n}\n\n.rte-bubble-separator {\n  width: 1px;\n  height: 1.25rem;\n  background: var(--border);\n  margin: 0 0.125rem;\n  flex-shrink: 0;\n}\n\n.rte-bubble-btn {\n  display: flex;\n  align-items: center;\n  gap: 0.25rem;\n  height: 1.75rem;\n  padding: 0 0.375rem;\n  border: none;\n  border-radius: calc(var(--radius) - 4px);\n  background: transparent;\n  color: var(--popover-foreground);\n  cursor: pointer;\n  font-size: 0.8125rem;\n  white-space: nowrap;\n  transition:\n    background-color 150ms ease-out,\n    color 150ms ease-out,\n    transform 100ms ease-out;\n}\n\n.rte-bubble-btn:hover {\n  background: var(--accent);\n  color: var(--accent-foreground);\n}\n\n.rte-bubble-btn:active {\n  transform: scale(0.96);\n}\n\n.rte-bubble-btn--active {\n  color: var(--primary);\n  background: var(--accent);\n}\n\n.rte-bubble-btn-icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n  width: 1rem;\n  height: 1rem;\n}\n\n.rte-bubble-btn-icon svg {\n  width: 100%;\n  height: 100%;\n}\n\n.rte-bubble-btn-text {\n  max-width: 5rem;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.rte-bubble-overlay {\n  position: fixed;\n  inset: 0;\n  z-index: 999;\n}\n\n.rte-bubble-dropdown {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: 1000;\n  min-width: 150px;\n  background: var(--popover);\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);\n  padding: 0.25rem;\n  margin-top: 4px;\n  transform-origin: top left;\n  animation: rte-bubble-pop-in 0.25s cubic-bezier(0.22, 1, 0.36, 1) forwards;\n}\n\n.rte-bubble-dropdown--language {\n  min-width: 160px;\n  max-height: 320px;\n  overflow-y: auto;\n  scrollbar-width: thin;\n}\n\n.rte-bubble-dropdown-item {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n  gap: 0.5rem;\n  position: relative;\n  z-index: 1;\n  width: 100%;\n  padding: 0.375rem 0.5rem;\n  font-size: 0.8125rem;\n  color: var(--muted-foreground);\n  background: transparent;\n  border: none;\n  border-radius: calc(var(--radius) - 4px);\n  cursor: pointer;\n  text-align: left;\n  transition:\n    background-color 100ms ease-out,\n    color 100ms ease-out;\n}\n\n.rte-bubble-dropdown-item:hover,\n.rte-bubble-dropdown-item:focus-visible {\n  color: var(--foreground);\n}\n\n.rte-bubble-dropdown-pill {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 0;\n  pointer-events: none;\n  border-radius: calc(var(--radius) - 4px);\n  background: color-mix(in srgb, var(--primary) 6%, transparent);\n  opacity: 0;\n  transition:\n    top 220ms cubic-bezier(0.22, 1, 0.36, 1),\n    left 220ms cubic-bezier(0.22, 1, 0.36, 1),\n    width 220ms cubic-bezier(0.22, 1, 0.36, 1),\n    height 220ms cubic-bezier(0.22, 1, 0.36, 1),\n    opacity 150ms ease-out;\n}\n\n.rte-bubble-dropdown-pill--visible {\n  opacity: 1;\n}\n\n.rte-bubble-dropdown-item--active {\n  color: var(--primary);\n  background-color: color-mix(in srgb, var(--primary) 10%, transparent);\n}\n\n.rte-bubble-dropdown-icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n  color: var(--muted-foreground);\n  width: 1rem;\n  height: 1rem;\n}\n\n.rte-bubble-dropdown-icon svg {\n  width: 100%;\n  height: 100%;\n}\n\n.rte-bubble-dropdown-item--active .rte-bubble-dropdown-icon {\n  color: var(--primary);\n}\n\n.rte-bubble-dropdown-divider {\n  height: 1px;\n  background: var(--border);\n  margin: 0.25rem 0;\n}\n\n.rte-bubble-dropdown--color {\n  min-width: auto;\n  padding: 0.5rem;\n}\n\n.rte-bubble-dropdown--color .rte-bubble-dropdown-divider {\n  margin: 0.5rem 0 0.375rem;\n}\n\n.rte-bubble-color-section {\n  display: flex;\n  flex-direction: column;\n  gap: 0.5rem;\n}\n\n.rte-bubble-color-label {\n  padding: 0 0.125rem;\n  color: var(--muted-foreground);\n  font-size: 0.75rem;\n  font-weight: 700;\n  line-height: 1;\n}\n\n.rte-bubble-color-grid {\n  display: grid;\n  grid-template-columns: repeat(5, 1.5rem);\n  justify-content: center;\n  gap: 0.375rem;\n}\n\n.rte-bubble-color-recent {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n  gap: 0.375rem;\n}\n\n.rte-bubble-color-swatch {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 1.5rem;\n  height: 1.5rem;\n  padding: 0;\n  border-radius: 999px;\n  border: 1px solid transparent;\n  cursor: pointer;\n  transition:\n    transform 120ms cubic-bezier(0.34, 1.56, 0.64, 1),\n    background-color 100ms ease-out,\n    border-color 100ms ease-out,\n    color 100ms ease-out,\n    box-shadow 120ms ease-out;\n}\n\n.rte-bubble-color-swatch:hover {\n  transform: scale(1.1);\n}\n\n.rte-bubble-color-swatch:active {\n  transform: scale(0.96);\n}\n\n.rte-bubble-color-swatch:focus-visible {\n  outline: 2px solid var(--ring);\n  outline-offset: 1px;\n}\n\n.rte-bubble-color-swatch--text {\n  background: transparent;\n  border-color: var(--swatch-color);\n  color: var(--swatch-color);\n}\n\n.rte-bubble-color-swatch--text.rte-bubble-color-swatch--active {\n  background: var(--swatch-color);\n  border-color: var(--swatch-color);\n  color: var(--swatch-check);\n}\n\n.rte-bubble-color-swatch--highlight {\n  background: var(--swatch-color);\n  border-color: transparent;\n  color: var(--swatch-check);\n}\n\n.rte-bubble-color-swatch--highlight.rte-bubble-color-swatch--active {\n  box-shadow: 0 0 0 2px\n    color-mix(in srgb, var(--swatch-color) 55%, var(--popover-foreground));\n}\n\n.rte-bubble-color-glyph {\n  font-size: 0.625rem;\n  font-weight: 700;\n  line-height: 1;\n}\n\n.rte-bubble-color-check {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 0.75rem;\n  height: 0.75rem;\n  color: var(--swatch-check);\n}\n\n.rte-bubble-color-check svg {\n  width: 100%;\n  height: 100%;\n}\n\n/* ── Suppress transitions on theme switch ── */\n.rte-theme-transitioning *,\n.rte-theme-transitioning *::before,\n.rte-theme-transitioning *::after {\n  transition: none !important;\n}\n",
      "path": "editor/style.css",
      "target": "@components/editor/style.css",
      "type": "registry:style"
    },
    {
      "content": "import { isTextSelection } from \"@tiptap/core\";\nimport type { Editor } from \"@tiptap/react\";\nimport { BubbleMenu as TiptapBubbleMenu } from \"@tiptap/react/menus\";\nimport { useCallback } from \"react\";\n\nimport { RteSeparator } from \"../ui\";\nimport { ColorSelector } from \"./color-selector\";\nimport { TextButtons } from \"./text-buttons\";\n\nexport interface BubbleMenuProps {\n  editor: Editor | null;\n}\n\nexport const BubbleMenu = ({ editor }: BubbleMenuProps) => {\n  const shouldShow = useCallback(\n    ({\n      editor: ed,\n      state,\n    }: {\n      editor: Editor;\n      state: { selection: { empty: boolean } };\n    }) => {\n      const { selection } = state;\n      if (!ed.isEditable) {\n        return false;\n      }\n      if (selection.empty || ed.isActive(\"codeBlock\")) {\n        return false;\n      }\n      if (!selection.empty && !isTextSelection(selection)) {\n        return false;\n      }\n      return true;\n    },\n    []\n  );\n\n  if (!editor) {\n    return null;\n  }\n\n  return (\n    <TiptapBubbleMenu\n      editor={editor}\n      options={{ offset: 8, placement: \"top\" }}\n      shouldShow={shouldShow}\n    >\n      <div className=\"rte-bubble-menu\">\n        <TextButtons editor={editor} />\n        <RteSeparator />\n        <ColorSelector editor={editor} />\n      </div>\n    </TiptapBubbleMenu>\n  );\n};\n",
      "path": "editor/bubble-menu/index.tsx",
      "target": "@components/editor/bubble-menu/index.tsx",
      "type": "registry:component"
    },
    {
      "content": "import type { Editor } from \"@tiptap/react\";\nimport { useCallback, useEffect, useRef, useSyncExternalStore } from \"react\";\n\nconst defaultEqual = <T>(a: T, b: T) => a === b;\n\nexport const shallowEqual = <T extends Record<string, unknown>>(\n  a: T,\n  b: T\n): boolean => {\n  if (a === b) {\n    return true;\n  }\n  if (!a || !b) {\n    return false;\n  }\n  const ak = Object.keys(a);\n  const bk = Object.keys(b);\n  if (ak.length !== bk.length) {\n    return false;\n  }\n  return ak.every((k) => a[k] === b[k]);\n};\n\nexport const useEditorState = <T>(\n  editor: Editor | null,\n  selector: (e: Editor) => T,\n  isEqual: (a: T, b: T) => boolean = defaultEqual\n): T => {\n  const selectorRef = useRef(selector);\n  const isEqualRef = useRef(isEqual);\n\n  useEffect(() => {\n    selectorRef.current = selector;\n    isEqualRef.current = isEqual;\n  });\n\n  const snapshotRef = useRef<{ value: T }>({\n    value: editor ? selector(editor) : (undefined as unknown as T),\n  });\n\n  const updateSnapshot = useCallback(() => {\n    if (!editor) {\n      return;\n    }\n    const next = selectorRef.current(editor);\n    if (\n      snapshotRef.current &&\n      isEqualRef.current(snapshotRef.current.value, next)\n    ) {\n      return;\n    }\n    snapshotRef.current = { value: next };\n  }, [editor]);\n\n  const subscribe = useCallback(\n    (onStoreChange: () => void) => {\n      if (!editor) {\n        return () => {\n          // noop\n        };\n      }\n      const update = () => {\n        updateSnapshot();\n        onStoreChange();\n      };\n      editor.on(\"selectionUpdate\", update);\n      editor.on(\"transaction\", update);\n      return () => {\n        editor.off(\"selectionUpdate\", update);\n        editor.off(\"transaction\", update);\n      };\n    },\n    [editor, updateSnapshot]\n  );\n\n  const getSnapshot = useCallback((): T => snapshotRef.current.value, []);\n\n  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n};\n",
      "path": "editor/bubble-menu/utils.ts",
      "target": "@components/editor/bubble-menu/utils.ts",
      "type": "registry:component"
    },
    {
      "content": "import type { Editor } from \"@tiptap/react\";\nimport { useState } from \"react\";\n\nimport {\n  RteButton,\n  RteColorSwatch,\n  RteDropdown,\n  RteDropdownDivider,\n  RteIcon,\n  RteOverlay,\n} from \"../ui\";\nimport { useEditorState, shallowEqual } from \"./utils\";\n\ntype SwatchType = \"text\" | \"highlight\";\n\ninterface RecentColor {\n  type: SwatchType;\n  color: string;\n  label: string;\n}\n\nconst RECENT_KEY = \"editorcn-editor-recent-colors\";\nconst RECENT_LIMIT = 10;\n\nconst readRecentColors = (): RecentColor[] => {\n  if (typeof window === \"undefined\") {\n    return [];\n  }\n  try {\n    const raw = window.localStorage.getItem(RECENT_KEY);\n    if (!raw) {\n      return [];\n    }\n    const parsed = JSON.parse(raw) as RecentColor[];\n    return Array.isArray(parsed) ? parsed.slice(0, RECENT_LIMIT) : [];\n  } catch {\n    return [];\n  }\n};\n\nconst persistRecentColors = (recent: RecentColor[]): void => {\n  try {\n    window.localStorage.setItem(RECENT_KEY, JSON.stringify(recent));\n  } catch {\n    /* ignored */\n  }\n};\n\nconst TEXT_COLORS = [\n  { color: \"#0f172a\", label: \"Foreground\" },\n  { color: \"#ef4444\", label: \"Red\" },\n  { color: \"#f97316\", label: \"Orange\" },\n  { color: \"#f59e0b\", label: \"Amber\" },\n  { color: \"#22c55e\", label: \"Green\" },\n  { color: \"#0ea5e9\", label: \"Sky\" },\n  { color: \"#3b82f6\", label: \"Blue\" },\n  { color: \"#6366f1\", label: \"Indigo\" },\n  { color: \"#a855f7\", label: \"Purple\" },\n  { color: \"#ec4899\", label: \"Pink\" },\n] as const;\n\nconst HIGHLIGHT_COLORS = [\n  { color: \"#fef08a\", label: \"Yellow\" },\n  { color: \"#fed7aa\", label: \"Orange\" },\n  { color: \"#fecaca\", label: \"Red\" },\n  { color: \"#fbcfe8\", label: \"Pink\" },\n  { color: \"#e9d5ff\", label: \"Purple\" },\n  { color: \"#ddd6fe\", label: \"Violet\" },\n  { color: \"#bfdbfe\", label: \"Blue\" },\n  { color: \"#a5f3fc\", label: \"Cyan\" },\n  { color: \"#99f6e4\", label: \"Teal\" },\n  { color: \"#bbf7d0\", label: \"Green\" },\n] as const;\n\ninterface ColorChain {\n  run(): boolean;\n  setColor(color: string): ColorChain;\n  setHighlight(attributes: { color: string }): ColorChain;\n  unsetColor(): ColorChain;\n  unsetHighlight(): ColorChain;\n}\n\nexport const ColorSelector = ({ editor }: { editor: Editor }) => {\n  const [open, setOpen] = useState(false);\n  const [recent, setRecent] = useState<RecentColor[]>(() => readRecentColors());\n\n  const { highlightColor, textColor } = useEditorState(\n    editor,\n    (ed) => ({\n      highlightColor: ed.isActive(\"highlight\")\n        ? (ed.getAttributes(\"highlight\").color as string | undefined)\n        : undefined,\n      textColor: ed.isActive(\"textStyle\")\n        ? (ed.getAttributes(\"textStyle\").color as string | undefined)\n        : undefined,\n    }),\n    shallowEqual\n  );\n\n  const hasColor = editor.extensionManager.extensions.some(\n    (ext) => ext.name === \"color\"\n  );\n  const hasHighlight = editor.extensionManager.extensions.some(\n    (ext) => ext.name === \"highlight\"\n  );\n\n  const remember = (type: SwatchType, color: string, label: string): void => {\n    setRecent((prev) => {\n      const next = [\n        { color, label, type },\n        ...prev.filter((item) => !(item.type === type && item.color === color)),\n      ].slice(0, RECENT_LIMIT);\n      persistRecentColors(next);\n      return next;\n    });\n  };\n\n  const selectText = (color: string, label: string): void => {\n    remember(\"text\", color, label);\n    if (textColor === color) {\n      (editor.chain().focus() as unknown as ColorChain).unsetColor().run();\n    } else {\n      (editor.chain().focus() as unknown as ColorChain).setColor(color).run();\n    }\n  };\n\n  const selectHighlight = (color: string, label: string): void => {\n    remember(\"highlight\", color, label);\n    if (highlightColor === color) {\n      (editor.chain().focus() as unknown as ColorChain).unsetHighlight().run();\n    } else {\n      (editor.chain().focus() as unknown as ColorChain)\n        .setHighlight({ color })\n        .run();\n    }\n  };\n\n  if (!hasColor && !hasHighlight) {\n    return null;\n  }\n\n  return (\n    <div style={{ position: \"relative\" }}>\n      <RteButton\n        active={Boolean(textColor || highlightColor)}\n        onMouseDown={(e) => e.preventDefault()}\n        onClick={() => setOpen(!open)}\n        title=\"Text color\"\n      >\n        <RteIcon>\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth={2}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className=\"rte-editor-icon\"\n          >\n            <path d=\"M4 20h16\" />\n            <path d=\"m6 16 6-12 6 12\" />\n            <path d=\"M8 12h8\" />\n          </svg>\n        </RteIcon>\n      </RteButton>\n      {open && (\n        <>\n          <RteOverlay\n            onClick={() => setOpen(false)}\n            onKeyDown={(e) => {\n              if (e.key === \"Escape\") {\n                setOpen(false);\n              }\n            }}\n          />\n          <RteDropdown variant=\"color\">\n            {recent.length > 0 ? (\n              <div className=\"rte-bubble-color-section\">\n                <span className=\"rte-bubble-color-label\">Recently Used</span>\n                <div className=\"rte-bubble-color-recent\">\n                  {recent.map((item) => (\n                    <RteColorSwatch\n                      key={`${item.type}-${item.color}`}\n                      variant={item.type}\n                      color={item.color}\n                      label={item.label}\n                      active={\n                        item.type === \"text\"\n                          ? textColor === item.color\n                          : highlightColor === item.color\n                      }\n                      onSelect={() =>\n                        item.type === \"text\"\n                          ? selectText(item.color, item.label)\n                          : selectHighlight(item.color, item.label)\n                      }\n                    />\n                  ))}\n                </div>\n              </div>\n            ) : null}\n            {hasColor ? (\n              <>\n                {recent.length > 0 && <RteDropdownDivider />}\n                <div className=\"rte-bubble-color-section\">\n                  <span className=\"rte-bubble-color-label\">Text Color</span>\n                  <div className=\"rte-bubble-color-grid\">\n                    {TEXT_COLORS.map((item) => (\n                      <RteColorSwatch\n                        key={item.color}\n                        variant=\"text\"\n                        color={item.color}\n                        label={item.label}\n                        active={textColor === item.color}\n                        onSelect={() => selectText(item.color, item.label)}\n                      />\n                    ))}\n                  </div>\n                </div>\n              </>\n            ) : null}\n            {hasHighlight ? (\n              <>\n                {(recent.length > 0 || hasColor) && <RteDropdownDivider />}\n                <div className=\"rte-bubble-color-section\">\n                  <span className=\"rte-bubble-color-label\">\n                    Highlight Color\n                  </span>\n                  <div className=\"rte-bubble-color-grid\">\n                    {HIGHLIGHT_COLORS.map((item) => (\n                      <RteColorSwatch\n                        key={item.color}\n                        variant=\"highlight\"\n                        color={item.color}\n                        label={item.label}\n                        active={highlightColor === item.color}\n                        onSelect={() => selectHighlight(item.color, item.label)}\n                      />\n                    ))}\n                  </div>\n                </div>\n              </>\n            ) : null}\n          </RteDropdown>\n        </>\n      )}\n    </div>\n  );\n};\n",
      "path": "editor/bubble-menu/color-selector.tsx",
      "target": "@components/editor/bubble-menu/color-selector.tsx",
      "type": "registry:component"
    },
    {
      "content": "import type { Editor } from \"@tiptap/react\";\n\nimport { RteButton, RteButtonGroup, RteIcon } from \"../ui\";\nimport { useEditorState, shallowEqual } from \"./utils\";\n\nconst Svg = ({\n  children,\n  strokeWidth = 2,\n}: {\n  children: React.ReactNode;\n  strokeWidth?: number;\n}) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth={strokeWidth}\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className=\"rte-editor-icon\"\n  >\n    {children}\n  </svg>\n);\n\nconst Icon = ({ children }: { children: React.ReactNode }) => (\n  <RteIcon>{children}</RteIcon>\n);\n\ninterface TextSelectorResult {\n  isBold: boolean;\n  isItalic: boolean;\n  isUnderline: boolean;\n  isStrike: boolean;\n  isCode: boolean;\n}\n\ninterface ToggleableChain {\n  run(): boolean;\n  toggleBold(): ToggleableChain;\n  toggleCode(): ToggleableChain;\n  toggleItalic(): ToggleableChain;\n  toggleStrike(): ToggleableChain;\n  toggleUnderline(): ToggleableChain;\n}\n\nconst textItems = [\n  {\n    command: (e: Editor) =>\n      (e.chain().focus() as unknown as ToggleableChain).toggleBold().run(),\n    icon: (\n      <Icon>\n        <Svg>\n          <path d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\" />\n          <path d=\"M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\" />\n        </Svg>\n      </Icon>\n    ),\n    isActive: (s: TextSelectorResult) => s.isBold,\n  },\n  {\n    command: (e: Editor) =>\n      (e.chain().focus() as unknown as ToggleableChain).toggleItalic().run(),\n    icon: (\n      <Icon>\n        <Svg>\n          <line x1=\"19\" y1=\"4\" x2=\"10\" y2=\"4\" />\n          <line x1=\"14\" y1=\"20\" x2=\"5\" y2=\"20\" />\n          <line x1=\"15\" y1=\"4\" x2=\"9\" y2=\"20\" />\n        </Svg>\n      </Icon>\n    ),\n    isActive: (s: TextSelectorResult) => s.isItalic,\n  },\n  {\n    command: (e: Editor) =>\n      (e.chain().focus() as unknown as ToggleableChain).toggleUnderline().run(),\n    icon: (\n      <Icon>\n        <Svg>\n          <path d=\"M6 4v6a6 6 0 0 0 12 0V4\" />\n          <line x1=\"4\" y1=\"20\" x2=\"20\" y2=\"20\" />\n        </Svg>\n      </Icon>\n    ),\n    isActive: (s: TextSelectorResult) => s.isUnderline,\n  },\n  {\n    command: (e: Editor) =>\n      (e.chain().focus() as unknown as ToggleableChain).toggleStrike().run(),\n    icon: (\n      <Icon>\n        <Svg>\n          <path d=\"M16 4H9.5a3.5 3.5 0 0 0-2.9 5.4\" />\n          <path d=\"M14.5 14.6a3.5 3.5 0 0 1-2.9 5.4H8\" />\n          <line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\" />\n        </Svg>\n      </Icon>\n    ),\n    isActive: (s: TextSelectorResult) => s.isStrike,\n  },\n  {\n    command: (e: Editor) =>\n      (e.chain().focus() as unknown as ToggleableChain).toggleCode().run(),\n    icon: (\n      <Icon>\n        <Svg>\n          <polyline points=\"16 18 22 12 16 6\" />\n          <polyline points=\"8 6 2 12 8 18\" />\n        </Svg>\n      </Icon>\n    ),\n    isActive: (s: TextSelectorResult) => s.isCode,\n  },\n];\n\nexport const TextButtons = ({ editor }: { editor: Editor }) => {\n  const editorState = useEditorState(\n    editor,\n    (ed) => ({\n      isBold: ed.isActive(\"bold\"),\n      isCode: ed.isActive(\"code\"),\n      isItalic: ed.isActive(\"italic\"),\n      isStrike: ed.isActive(\"strike\"),\n      isUnderline: ed.isActive(\"underline\"),\n    }),\n    shallowEqual\n  );\n\n  return (\n    <RteButtonGroup>\n      {textItems.map((item, i) => (\n        <RteButton\n          key={i}\n          active={item.isActive(editorState)}\n          onMouseDown={(e) => e.preventDefault()}\n          onClick={() => item.command(editor)}\n        >\n          {item.icon}\n        </RteButton>\n      ))}\n    </RteButtonGroup>\n  );\n};\n",
      "path": "editor/bubble-menu/text-buttons.tsx",
      "target": "@components/editor/bubble-menu/text-buttons.tsx",
      "type": "registry:component"
    },
    {
      "content": "import type { ChainedCommands, Editor } from \"@tiptap/core\";\nimport { useEditorState } from \"@tiptap/react\";\nimport React, { useContext } from \"react\";\n\nimport type { RichTextEditorIcons } from \"../icon-types\";\nimport type { RichTextEditorLabels } from \"../labels\";\nimport {\n  RichTextEditorContext,\n  useRichTextEditorContext,\n} from \"../rte-context\";\nimport type { RichTextEditorControlProps } from \"../types\";\nimport { Toggle } from \"../ui/toggle\";\nimport { TooltipTrigger } from \"../ui/tooltip\";\nimport { cn } from \"../ui/utils\";\n\ntype IsActiveConfig =\n  | { name: string; attributes?: Record<string, unknown> | string }\n  | { attrs: Record<string, unknown> };\n\ntype ChainCommand = (\n  attributes?: Record<string, unknown> | string\n) => Pick<ChainedCommands, \"run\">;\n\ninterface CreateControlProps {\n  label: keyof RichTextEditorLabels;\n  iconKey: Exclude<keyof RichTextEditorIcons, \"languageIcons\">;\n  isActive?: IsActiveConfig;\n  isDisabled?: (editor: Editor) => boolean;\n  operation: { name: string; attributes?: Record<string, unknown> | string };\n}\n\nexport const RichTextEditorTooltip = ({\n  children,\n  label,\n}: {\n  children: React.ReactElement;\n  label: string;\n}) => {\n  const tooltip = useContext(RichTextEditorContext)?.tooltip;\n  if (!tooltip) {\n    return children;\n  }\n  return <TooltipTrigger handle={tooltip} payload={label} render={children} />;\n};\n\nexport const RichTextEditorControl = ({\n  active = false,\n  interactive: _interactive = true,\n  className,\n  children,\n  onMouseDown,\n  onClick,\n  disabled,\n  ...props\n}: RichTextEditorControlProps) => {\n  const toggle = (\n    <Toggle\n      size=\"sm\"\n      pressed={active}\n      disabled={disabled}\n      aria-label={props[\"aria-label\"]}\n      className={cn(\"rte-control-button\", className)}\n      onMouseDown={(e) => {\n        e.preventDefault();\n        onMouseDown?.(e);\n      }}\n      onPressedChange={() =>\n        onClick?.({} as React.MouseEvent<HTMLButtonElement>)\n      }\n    >\n      {children}\n    </Toggle>\n  );\n\n  return props.title ? (\n    <RichTextEditorTooltip label={props.title}>{toggle}</RichTextEditorTooltip>\n  ) : (\n    toggle\n  );\n};\n\ntype EditorStateSelector = (ctx: { editor: Editor | null }) => {\n  active: boolean;\n  disabled: boolean;\n};\n\nconst resolveIsActive = (\n  editor: Editor | null,\n  config?: IsActiveConfig\n): boolean => {\n  if (!editor || !config) {\n    return false;\n  }\n  if (\"attrs\" in config) {\n    return editor.isActive(config.attrs);\n  }\n  return editor.isActive(config.name, config.attributes);\n};\n\nconst createSelector =\n  (\n    isActive?: IsActiveConfig,\n    isDisabled?: (editor: Editor) => boolean\n  ): EditorStateSelector =>\n  (ctx) => {\n    const safeEditor =\n      ctx.editor && !ctx.editor.isDestroyed ? ctx.editor : null;\n\n    return {\n      active: resolveIsActive(safeEditor, isActive),\n      disabled: safeEditor ? (isDisabled?.(safeEditor) ?? false) : true,\n    };\n  };\n\nexport const createControl = ({\n  label,\n  iconKey,\n  isActive,\n  isDisabled,\n  operation,\n}: CreateControlProps) => {\n  const Control = ({ className }: { className?: string }) => {\n    const { editor, labels, icons } = useRichTextEditorContext();\n    const ariaLabel = labels[label] as string;\n\n    const selector = createSelector(isActive, isDisabled);\n    useEditorState({ editor: editor ?? null, selector });\n    const { active, disabled } = selector({ editor: editor ?? null });\n\n    return (\n      <RichTextEditorControl\n        active={active}\n        disabled={disabled}\n        aria-label={ariaLabel}\n        title={ariaLabel}\n        className={className}\n        onClick={() => {\n          if (!editor || editor.isDestroyed) {\n            return;\n          }\n          const commands = editor.chain().focus() as unknown as Record<\n            string,\n            ChainCommand\n          >;\n          commands[operation.name]?.(operation.attributes).run();\n        }}\n      >\n        {icons[iconKey]}\n      </RichTextEditorControl>\n    );\n  };\n\n  Control.displayName = `RichTextEditor.${String(label)}`;\n  return Control;\n};\n",
      "path": "editor/controls/rte-control.tsx",
      "target": "@components/editor/controls/rte-control.tsx",
      "type": "registry:component"
    },
    {
      "content": "import { createControl } from \"./rte-control\";\n\ninterface HistoryCanCommands {\n  redo: () => boolean;\n  undo: () => boolean;\n}\n\nexport const BoldControl = createControl({\n  iconKey: \"boldControlIcon\",\n  isActive: { name: \"bold\" },\n  label: \"boldControlLabel\",\n  operation: { name: \"toggleBold\" },\n});\n\nexport const ItalicControl = createControl({\n  iconKey: \"italicControlIcon\",\n  isActive: { name: \"italic\" },\n  label: \"italicControlLabel\",\n  operation: { name: \"toggleItalic\" },\n});\n\nexport const UnderlineControl = createControl({\n  iconKey: \"underlineControlIcon\",\n  isActive: { name: \"underline\" },\n  label: \"underlineControlLabel\",\n  operation: { name: \"toggleUnderline\" },\n});\n\nexport const StrikethroughControl = createControl({\n  iconKey: \"strikeControlIcon\",\n  isActive: { name: \"strike\" },\n  label: \"strikeControlLabel\",\n  operation: { name: \"toggleStrike\" },\n});\n\nexport const ClearFormattingControl = createControl({\n  iconKey: \"clearFormattingControlIcon\",\n  label: \"clearFormattingControlLabel\",\n  operation: { name: \"unsetAllMarks\" },\n});\n\nexport const CodeControl = createControl({\n  iconKey: \"codeControlIcon\",\n  isActive: { name: \"code\" },\n  label: \"codeControlLabel\",\n  operation: { name: \"toggleCode\" },\n});\n\nexport const CodeBlockControl = createControl({\n  iconKey: \"codeBlockControlIcon\",\n  isActive: { name: \"codeBlock\" },\n  label: \"codeBlockControlLabel\",\n  operation: { name: \"toggleCodeBlock\" },\n});\n\nexport const H1Control = createControl({\n  iconKey: \"h1ControlIcon\",\n  isActive: { attributes: { level: 1 }, name: \"heading\" },\n  label: \"h1ControlLabel\",\n  operation: { attributes: { level: 1 }, name: \"toggleHeading\" },\n});\n\nexport const H2Control = createControl({\n  iconKey: \"h2ControlIcon\",\n  isActive: { attributes: { level: 2 }, name: \"heading\" },\n  label: \"h2ControlLabel\",\n  operation: { attributes: { level: 2 }, name: \"toggleHeading\" },\n});\n\nexport const H3Control = createControl({\n  iconKey: \"h3ControlIcon\",\n  isActive: { attributes: { level: 3 }, name: \"heading\" },\n  label: \"h3ControlLabel\",\n  operation: { attributes: { level: 3 }, name: \"toggleHeading\" },\n});\n\nexport const H4Control = createControl({\n  iconKey: \"h4ControlIcon\",\n  isActive: { attributes: { level: 4 }, name: \"heading\" },\n  label: \"h4ControlLabel\",\n  operation: { attributes: { level: 4 }, name: \"toggleHeading\" },\n});\n\nexport const BulletListControl = createControl({\n  iconKey: \"bulletListControlIcon\",\n  isActive: { name: \"bulletList\" },\n  label: \"bulletListControlLabel\",\n  operation: { name: \"toggleBulletList\" },\n});\n\nexport const OrderedListControl = createControl({\n  iconKey: \"orderedListControlIcon\",\n  isActive: { name: \"orderedList\" },\n  label: \"orderedListControlLabel\",\n  operation: { name: \"toggleOrderedList\" },\n});\n\nexport const BlockquoteControl = createControl({\n  iconKey: \"blockquoteControlIcon\",\n  isActive: { name: \"blockquote\" },\n  label: \"blockquoteControlLabel\",\n  operation: { name: \"toggleBlockquote\" },\n});\n\nexport const HrControl = createControl({\n  iconKey: \"hrControlIcon\",\n  label: \"hrControlLabel\",\n  operation: { name: \"setHorizontalRule\" },\n});\n\nexport const UnlinkControl = createControl({\n  iconKey: \"unlinkControlIcon\",\n  label: \"unlinkControlLabel\",\n  operation: { name: \"unsetLink\" },\n});\n\nexport const UndoControl = createControl({\n  iconKey: \"undoControlIcon\",\n  isDisabled: (editor) =>\n    !(editor.can() as unknown as HistoryCanCommands).undo(),\n  label: \"undoControlLabel\",\n  operation: { name: \"undo\" },\n});\n\nexport const RedoControl = createControl({\n  iconKey: \"redoControlIcon\",\n  isDisabled: (editor) =>\n    !(editor.can() as unknown as HistoryCanCommands).redo(),\n  label: \"redoControlLabel\",\n  operation: { name: \"redo\" },\n});\n\nexport const H5Control = createControl({\n  iconKey: \"h5ControlIcon\",\n  isActive: { attributes: { level: 5 }, name: \"heading\" },\n  label: \"h5ControlLabel\",\n  operation: { attributes: { level: 5 }, name: \"toggleHeading\" },\n});\n\nexport const H6Control = createControl({\n  iconKey: \"h6ControlIcon\",\n  isActive: { attributes: { level: 6 }, name: \"heading\" },\n  label: \"h6ControlLabel\",\n  operation: { attributes: { level: 6 }, name: \"toggleHeading\" },\n});\n\nexport const AlignLeftControl = createControl({\n  iconKey: \"alignLeftControlIcon\",\n  isActive: { attrs: { textAlign: \"left\" } },\n  label: \"alignLeftControlLabel\",\n  operation: { attributes: \"left\", name: \"setTextAlign\" },\n});\n\nexport const AlignCenterControl = createControl({\n  iconKey: \"alignCenterControlIcon\",\n  isActive: { attrs: { textAlign: \"center\" } },\n  label: \"alignCenterControlLabel\",\n  operation: { attributes: \"center\", name: \"setTextAlign\" },\n});\n\nexport const AlignRightControl = createControl({\n  iconKey: \"alignRightControlIcon\",\n  isActive: { attrs: { textAlign: \"right\" } },\n  label: \"alignRightControlLabel\",\n  operation: { attributes: \"right\", name: \"setTextAlign\" },\n});\n\nexport const AlignJustifyControl = createControl({\n  iconKey: \"alignJustifyControlIcon\",\n  isActive: { attrs: { textAlign: \"justify\" } },\n  label: \"alignJustifyControlLabel\",\n  operation: { attributes: \"justify\", name: \"setTextAlign\" },\n});\n\nexport const HighlightControl = createControl({\n  iconKey: \"highlightControlIcon\",\n  isActive: { name: \"highlight\" },\n  label: \"highlightControlLabel\",\n  operation: { name: \"toggleHighlight\" },\n});\n\nexport const SubscriptControl = createControl({\n  iconKey: \"subscriptControlIcon\",\n  isActive: { name: \"subscript\" },\n  label: \"subscriptControlLabel\",\n  operation: { name: \"toggleSubscript\" },\n});\n\nexport const SuperscriptControl = createControl({\n  iconKey: \"superscriptControlIcon\",\n  isActive: { name: \"superscript\" },\n  label: \"superscriptControlLabel\",\n  operation: { name: \"toggleSuperscript\" },\n});\n",
      "path": "editor/controls/rte-controls.tsx",
      "target": "@components/editor/controls/rte-controls.tsx",
      "type": "registry:component"
    },
    {
      "content": "import { useEditorState } from \"@tiptap/react\";\nimport * as React from \"react\";\nimport { useCallback, useState } from \"react\";\n\nimport { useRichTextEditorContext } from \"../rte-context\";\nimport { Button } from \"../ui/button\";\nimport { Input } from \"../ui/input\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover\";\nimport { Toggle } from \"../ui/toggle\";\nimport { TooltipTrigger } from \"../ui/tooltip\";\n\nexport const LinkControl = () => {\n  const { editor, icons, labels, tooltip } = useRichTextEditorContext();\n  const [url, setUrl] = useState(\"\");\n  const [open, setOpen] = useState(false);\n\n  const active = useEditorState({\n    editor: editor ?? null,\n    selector: (ctx) =>\n      ctx.editor && !ctx.editor.isDestroyed\n        ? ctx.editor.isActive(\"link\")\n        : false,\n  });\n\n  const handleOpen = useCallback(\n    (isOpen: boolean) => {\n      if (isOpen) {\n        const linkData = editor?.getAttributes(\"link\");\n        setUrl(linkData?.href || \"\");\n      } else {\n        setUrl(\"\");\n      }\n      setOpen(isOpen);\n    },\n    [editor]\n  );\n\n  const handleSave = () => {\n    if (!editor || editor.isDestroyed) {\n      return;\n    }\n    if (url === \"\") {\n      editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n    } else {\n      editor\n        .chain()\n        .focus()\n        .extendMarkRange(\"link\")\n        .setLink({ href: url })\n        .run();\n    }\n    setOpen(false);\n    setUrl(\"\");\n  };\n\n  const handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void = (\n    e\n  ) => {\n    if (e.key === \"Enter\") {\n      e.preventDefault();\n      handleSave();\n    }\n  };\n\n  React.useEffect(() => {\n    const handler = () => handleOpen(true);\n    window.addEventListener(\"edit-link\", handler);\n    return () => window.removeEventListener(\"edit-link\", handler);\n  }, [handleOpen]);\n\n  return (\n    <Popover open={open} onOpenChange={handleOpen}>\n      <TooltipTrigger\n        handle={tooltip}\n        payload={labels.linkControlLabel}\n        render={\n          <PopoverTrigger\n            render={\n              <Toggle\n                size=\"sm\"\n                pressed={active ?? false}\n                aria-label={labels.linkControlLabel}\n                className=\"p-0\"\n              />\n            }\n            onMouseDown={(e: React.MouseEvent) => e.preventDefault()}\n          >\n            {icons.linkControlIcon}\n          </PopoverTrigger>\n        }\n      />\n      <PopoverContent className=\"w-72 p-3\">\n        <div className=\"rte-link-editor\">\n          <Input\n            type=\"url\"\n            placeholder={labels.linkEditorInputPlaceholder}\n            aria-label={labels.linkEditorInputLabel}\n            value={url}\n            onChange={(e) => setUrl(e.target.value)}\n            onKeyDown={handleKeyDown}\n            className=\"rte-link-editor-input h-8 text-sm\"\n          />\n          <Button\n            size=\"sm\"\n            variant=\"default\"\n            onClick={handleSave}\n            className=\"rte-link-editor-save h-8\"\n          >\n            {labels.linkEditorSave}\n          </Button>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n};\n",
      "path": "editor/controls/rte-link-control.tsx",
      "target": "@components/editor/controls/rte-link-control.tsx",
      "type": "registry:component"
    },
    {
      "content": "export { Link } from \"./link\";\nexport { ResizableNodeView } from \"./resizable-node-view\";\n",
      "path": "editor/extensions/index.ts",
      "target": "@components/editor/extensions/index.ts",
      "type": "registry:component"
    },
    {
      "content": "import TipTapLink from \"@tiptap/extension-link\";\n\nexport const Link = TipTapLink.extend({\n  addKeyboardShortcuts() {\n    return {\n      \"Mod-k\": () => {\n        window.dispatchEvent(new Event(\"edit-link\"));\n        return true;\n      },\n    };\n  },\n}).configure({ openOnClick: false });\n",
      "path": "editor/extensions/link.ts",
      "target": "@components/editor/extensions/link.ts",
      "type": "registry:component"
    },
    {
      "content": "\"use client\";\n\nimport { NodeViewWrapper } from \"@tiptap/react\";\nimport type { NodeViewProps } from \"@tiptap/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n  AlignIcon,\n  DeleteEmbedIcon,\n  EditUrlIcon,\n  OpenInYouTubeIcon,\n} from \"../icons\";\n\ninterface ResizableNodeViewProps extends NodeViewProps {\n  children: React.ReactNode;\n  lockAspect?: boolean;\n  aspectRatio?: number;\n  minWidth?: number;\n  maxWidth?: number;\n  videoSrc?: string;\n  watchUrl?: string;\n  onSrcChange?: (src: string) => void;\n}\n\ntype ResizeDirection = \"left\" | \"right\" | \"bottom\" | \"corner-bl\" | \"corner-br\";\n\ninterface EmbedToolbarProps {\n  align: string;\n  videoSrc: string | undefined;\n  watchUrl: string | undefined;\n  deleteNode: (() => void) | undefined;\n  inputRef: React.RefObject<HTMLInputElement | null>;\n  isEditingUrl: boolean;\n  urlInput: string;\n  setUrlInput: (v: string) => void;\n  setIsEditingUrl: (v: boolean) => void;\n  handleSaveUrl: () => void;\n  handleApplyPreset: (pct: number) => void;\n  updateAttributes: (attrs: Record<string, unknown>) => void;\n  nodeSrc: string | undefined;\n}\n\nconst EmbedToolbar = ({\n  align,\n  videoSrc,\n  watchUrl,\n  deleteNode,\n  inputRef,\n  isEditingUrl,\n  urlInput,\n  setUrlInput,\n  setIsEditingUrl,\n  handleSaveUrl,\n  handleApplyPreset,\n  updateAttributes,\n  nodeSrc,\n}: EmbedToolbarProps) => {\n  if (isEditingUrl) {\n    return (\n      <div className=\"rte-embed-selector-edit-form\">\n        <input\n          ref={inputRef}\n          type=\"text\"\n          value={urlInput}\n          onChange={(e) => setUrlInput(e.target.value)}\n          placeholder=\"Paste YouTube URL or video ID...\"\n          className=\"rte-embed-selector-input\"\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") {\n              handleSaveUrl();\n            } else if (e.key === \"Escape\") {\n              setIsEditingUrl(false);\n            }\n          }}\n        />\n        <button\n          type=\"button\"\n          onClick={handleSaveUrl}\n          className=\"rte-embed-selector-btn rte-embed-selector-btn--save\"\n        >\n          Save\n        </button>\n        <button\n          type=\"button\"\n          onClick={() => setIsEditingUrl(false)}\n          className=\"rte-embed-selector-btn\"\n        >\n          Cancel\n        </button>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"rte-embed-selector-group\">\n        {([\"left\", \"center\", \"right\"] as const).map((dir) => (\n          <button\n            key={dir}\n            type=\"button\"\n            title={`Align ${dir.charAt(0).toUpperCase()}${dir.slice(1)}`}\n            aria-label={`Align ${dir.charAt(0).toUpperCase()}${dir.slice(1)}`}\n            className={`rte-embed-selector-btn ${align === dir ? \"rte-embed-selector-btn--active\" : \"\"}`}\n            onClick={() => updateAttributes({ align: dir })}\n          >\n            <AlignIcon dir={dir} />\n          </button>\n        ))}\n      </div>\n\n      <div className=\"rte-embed-selector-divider\" />\n\n      <div className=\"rte-embed-selector-group\">\n        {([25, 50, 75, 100] as const).map((pct) => (\n          <button\n            key={pct}\n            type=\"button\"\n            title={`${pct}%`}\n            className=\"rte-embed-selector-btn rte-embed-selector-btn--preset\"\n            onClick={() => handleApplyPreset(pct)}\n          >\n            {pct}%\n          </button>\n        ))}\n      </div>\n\n      <div className=\"rte-embed-selector-divider\" />\n\n      <div className=\"rte-embed-selector-group\">\n        {(videoSrc || nodeSrc) && (\n          <button\n            type=\"button\"\n            title=\"Edit URL\"\n            aria-label=\"Edit URL\"\n            className=\"rte-embed-selector-btn\"\n            onClick={() => {\n              setUrlInput(videoSrc || nodeSrc || \"\");\n              setIsEditingUrl(true);\n            }}\n          >\n            <EditUrlIcon />\n          </button>\n        )}\n        {watchUrl && (\n          <button\n            type=\"button\"\n            title=\"Open video in YouTube\"\n            aria-label=\"Open video in YouTube\"\n            className=\"rte-embed-selector-btn\"\n            onClick={() => window.open(watchUrl, \"_blank\")}\n          >\n            <OpenInYouTubeIcon />\n          </button>\n        )}\n        {deleteNode && (\n          <button\n            type=\"button\"\n            title=\"Delete Embed\"\n            aria-label=\"Delete Embed\"\n            className=\"rte-embed-selector-btn rte-embed-selector-btn--danger\"\n            onClick={() => deleteNode()}\n          >\n            <DeleteEmbedIcon />\n          </button>\n        )}\n      </div>\n    </>\n  );\n};\n\nexport const ResizableNodeView = ({\n  node,\n  selected,\n  updateAttributes,\n  deleteNode,\n  editor,\n  view,\n  children,\n  lockAspect = false,\n  aspectRatio = 16 / 9,\n  minWidth = 300,\n  maxWidth = 1200,\n  videoSrc,\n  watchUrl,\n  onSrcChange,\n}: ResizableNodeViewProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [isEditingUrl, setIsEditingUrl] = useState(false);\n  const [urlInput, setUrlInput] = useState(videoSrc || \"\");\n  const [isResizing, setIsResizing] = useState(false);\n  const [currentDimensions, setCurrentDimensions] = useState<{\n    w: number;\n    h: number;\n  } | null>(null);\n\n  const resizingRef = useRef<{\n    direction: ResizeDirection;\n    startX: number;\n    startY: number;\n    startWidth: number;\n    startHeight: number;\n    editorWidth: number;\n  } | null>(null);\n\n  const width = node.attrs.width as number | undefined;\n  const height = node.attrs.height as number | undefined;\n  const align = (node.attrs.align as string) || \"center\";\n\n  const getEditorWidth = useCallback(\n    () => view?.dom?.parentElement?.offsetWidth ?? 800,\n    [view]\n  );\n\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    if (isEditingUrl) {\n      inputRef.current?.focus();\n    }\n  }, [isEditingUrl]);\n\n  const handleMouseMove = useCallback(\n    (e: MouseEvent) => {\n      const resize = resizingRef.current;\n      if (!resize) {\n        return;\n      }\n\n      const effectiveMax = Math.min(maxWidth, resize.editorWidth);\n\n      let newWidth = resize.startWidth;\n      let newHeight = resize.startHeight;\n\n      if (resize.direction === \"right\" || resize.direction === \"corner-br\") {\n        newWidth = Math.max(\n          minWidth,\n          Math.min(\n            effectiveMax,\n            resize.startWidth + (e.clientX - resize.startX)\n          )\n        );\n      } else if (\n        resize.direction === \"left\" ||\n        resize.direction === \"corner-bl\"\n      ) {\n        newWidth = Math.max(\n          minWidth,\n          Math.min(\n            effectiveMax,\n            resize.startWidth - (e.clientX - resize.startX)\n          )\n        );\n      }\n\n      if (\n        resize.direction === \"bottom\" ||\n        resize.direction === \"corner-br\" ||\n        resize.direction === \"corner-bl\"\n      ) {\n        newHeight = Math.max(\n          Math.round(minWidth / aspectRatio),\n          Math.min(\n            Math.round(effectiveMax / aspectRatio),\n            resize.startHeight + (e.clientY - resize.startY)\n          )\n        );\n      }\n\n      if (lockAspect) {\n        if (\n          resize.direction === \"left\" ||\n          resize.direction === \"right\" ||\n          resize.direction === \"corner-br\" ||\n          resize.direction === \"corner-bl\"\n        ) {\n          newHeight = Math.round(newWidth / aspectRatio);\n        } else {\n          newWidth = Math.round(newHeight * aspectRatio);\n        }\n      }\n\n      if (containerRef.current) {\n        containerRef.current.style.width = `${newWidth}px`;\n        containerRef.current.style.height = `${newHeight}px`;\n      }\n      setCurrentDimensions({ h: newHeight, w: newWidth });\n    },\n    [lockAspect, aspectRatio, minWidth, maxWidth]\n  );\n\n  const handleMouseUp = useCallback(\n    function handleMouseUp() {\n      const resize = resizingRef.current;\n      if (!resize) {\n        return;\n      }\n\n      document.removeEventListener(\"mousemove\", handleMouseMove);\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n      document.body.style.cursor = \"\";\n      document.body.style.userSelect = \"\";\n\n      if (containerRef.current) {\n        const w = containerRef.current.offsetWidth;\n        const h = containerRef.current.offsetHeight;\n        updateAttributes({ height: h, width: w });\n      }\n\n      resizingRef.current = null;\n      setIsResizing(false);\n      setCurrentDimensions(null);\n    },\n    [handleMouseMove, updateAttributes]\n  );\n\n  const handleMouseDown = useCallback(\n    (e: React.MouseEvent, direction: ResizeDirection) => {\n      e.preventDefault();\n      e.stopPropagation();\n\n      const editorWidth = getEditorWidth();\n      const startWidth =\n        containerRef.current?.offsetWidth ??\n        width ??\n        Math.min(560, editorWidth);\n      const startHeight = containerRef.current?.offsetHeight ?? height ?? 315;\n\n      resizingRef.current = {\n        direction,\n        editorWidth,\n        startHeight,\n        startWidth,\n        startX: e.clientX,\n        startY: e.clientY,\n      };\n\n      setIsResizing(true);\n      setCurrentDimensions({ h: startHeight, w: startWidth });\n\n      document.addEventListener(\"mousemove\", handleMouseMove);\n      document.addEventListener(\"mouseup\", handleMouseUp);\n\n      const cursors: Record<ResizeDirection, string> = {\n        bottom: \"ns-resize\",\n        \"corner-bl\": \"nesw-resize\",\n        \"corner-br\": \"nwse-resize\",\n        left: \"ew-resize\",\n        right: \"ew-resize\",\n      };\n      document.body.style.cursor = cursors[direction];\n      document.body.style.userSelect = \"none\";\n    },\n    [width, height, getEditorWidth, handleMouseMove, handleMouseUp]\n  );\n\n  useEffect(\n    () => () => {\n      document.removeEventListener(\"mousemove\", handleMouseMove);\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n      document.body.style.cursor = \"\";\n      document.body.style.userSelect = \"\";\n    },\n    [handleMouseMove, handleMouseUp]\n  );\n\n  const handleApplyPreset = (percent: number) => {\n    const editorW = getEditorWidth() || 800;\n    const targetW = Math.round(\n      Math.max(minWidth, Math.min(maxWidth, (editorW * percent) / 100))\n    );\n    updateAttributes({\n      height: Math.round(targetW / aspectRatio),\n      width: targetW,\n    });\n  };\n\n  const handleSaveUrl = () => {\n    if (urlInput && onSrcChange) {\n      onSrcChange(urlInput);\n    } else if (urlInput) {\n      updateAttributes({ src: urlInput });\n    }\n    setIsEditingUrl(false);\n  };\n\n  const isEditable = editor ? editor.isEditable : true;\n\n  return (\n    <NodeViewWrapper\n      className=\"rte-embed-wrapper\"\n      data-selected={selected || undefined}\n      data-align={align}\n    >\n      <div\n        ref={containerRef}\n        className=\"rte-embed\"\n        contentEditable={false}\n        style={{\n          height: height ? `${height}px` : \"auto\",\n          minWidth: `${minWidth}px`,\n          width: width ? `${width}px` : \"100%\",\n        }}\n      >\n        {children}\n\n        {selected && isEditable && (\n          <div className=\"rte-embed-selector\" contentEditable={false}>\n            <EmbedToolbar\n              align={align}\n              videoSrc={videoSrc}\n              watchUrl={watchUrl}\n              deleteNode={deleteNode}\n              inputRef={inputRef}\n              isEditingUrl={isEditingUrl}\n              urlInput={urlInput}\n              setUrlInput={setUrlInput}\n              setIsEditingUrl={setIsEditingUrl}\n              handleSaveUrl={handleSaveUrl}\n              handleApplyPreset={handleApplyPreset}\n              updateAttributes={updateAttributes}\n              nodeSrc={node.attrs.src}\n            />\n          </div>\n        )}\n\n        {isResizing && currentDimensions && (\n          <div className=\"rte-embed-resize-badge\">\n            {currentDimensions.w} × {currentDimensions.h} px\n          </div>\n        )}\n\n        {selected && isEditable && (\n          <>\n            <div\n              className=\"rte-resize-handle rte-resize-handle--left\"\n              onMouseDown={(e) => handleMouseDown(e, \"left\")}\n              role=\"presentation\"\n            />\n            <div\n              className=\"rte-resize-handle rte-resize-handle--right\"\n              onMouseDown={(e) => handleMouseDown(e, \"right\")}\n              role=\"presentation\"\n            />\n            <div\n              className=\"rte-resize-handle rte-resize-handle--bottom\"\n              onMouseDown={(e) => handleMouseDown(e, \"bottom\")}\n              role=\"presentation\"\n            />\n            <div\n              className=\"rte-resize-handle rte-resize-handle--corner rte-resize-handle--corner-bl\"\n              onMouseDown={(e) => handleMouseDown(e, \"corner-bl\")}\n              role=\"presentation\"\n            />\n            <div\n              className=\"rte-resize-handle rte-resize-handle--corner rte-resize-handle--corner-br\"\n              onMouseDown={(e) => handleMouseDown(e, \"corner-br\")}\n              role=\"presentation\"\n            />\n          </>\n        )}\n      </div>\n    </NodeViewWrapper>\n  );\n};\n",
      "path": "editor/extensions/resizable-node-view.tsx",
      "target": "@components/editor/extensions/resizable-node-view.tsx",
      "type": "registry:component"
    },
    {
      "content": "\"use client\";\n\nimport {\n  createAtomBlockMarkdownSpec,\n  Node,\n  nodeInputRule,\n  parseAttributes,\n} from \"@tiptap/core\";\nimport { ReactNodeViewRenderer, useEditorState } from \"@tiptap/react\";\nimport type { NodeViewProps } from \"@tiptap/react\";\nimport { useState, useCallback, useEffect, useRef } from \"react\";\n\nimport { RichTextEditorControl } from \"../controls/rte-control\";\nimport { ResizableNodeView } from \"../extensions/resizable-node-view\";\nimport { useRichTextEditorContext } from \"../rte-context\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"../ui/dialog\";\nimport { Input } from \"../ui/input\";\n\nconst WIDGET_SCRIPT_URL = \"https://platform.twitter.com/widgets.js\";\nconst SCRIPT_LOAD_TIMEOUT_MS = 8000;\nconst READY_POLL_INTERVAL_MS = 50;\n\nlet twitterScriptPromise: Promise<void> | null = null;\n\nconst loadTwitterScript = (): Promise<void> => {\n  if (typeof window === \"undefined\") {\n    return Promise.resolve();\n  }\n  if (window.twttr?.widgets) {\n    return Promise.resolve();\n  }\n  if (twitterScriptPromise) {\n    return twitterScriptPromise;\n  }\n\n  // eslint-disable-next-line promise/avoid-new\n  twitterScriptPromise = new Promise<void>((resolve, reject) => {\n    let script = document.querySelector<HTMLScriptElement>(\n      `script[src=\"${WIDGET_SCRIPT_URL}\"]`\n    );\n    const isNewScript = !script;\n    if (!script) {\n      script = document.createElement(\"script\");\n      script.src = WIDGET_SCRIPT_URL;\n      script.async = true;\n      document.body.append(script);\n    }\n\n    let settled = false;\n    const timers: {\n      poll?: ReturnType<typeof setInterval>;\n      timeout?: ReturnType<typeof setTimeout>;\n    } = {};\n\n    const cleanup = () => {\n      clearInterval(timers.poll);\n      clearTimeout(timers.timeout);\n    };\n\n    const succeed = () => {\n      if (settled) {\n        return;\n      }\n      settled = true;\n      cleanup();\n      resolve();\n    };\n\n    const fail = () => {\n      if (settled) {\n        return;\n      }\n      settled = true;\n      cleanup();\n      twitterScriptPromise = null;\n      reject(new Error(\"Failed to load Twitter widgets script\"));\n    };\n\n    if (window.twttr?.widgets) {\n      succeed();\n      return;\n    }\n\n    timers.poll = setInterval(() => {\n      if (window.twttr?.widgets) {\n        succeed();\n      }\n    }, READY_POLL_INTERVAL_MS);\n\n    timers.timeout = setTimeout(fail, SCRIPT_LOAD_TIMEOUT_MS);\n\n    if (isNewScript) {\n      script.addEventListener(\n        \"load\",\n        () => {\n          if (window.twttr?.widgets) {\n            succeed();\n          }\n          // If widgets isn't attached yet, the poll above will pick it up.\n        },\n        { once: true }\n      );\n    }\n\n    script.addEventListener(\"error\", fail, { once: true });\n  });\n\n  return twitterScriptPromise;\n};\n\ndeclare global {\n  interface Window {\n    twttr?: {\n      widgets?: {\n        createTweet: (\n          tweetId: string,\n          container: HTMLElement\n        ) => Promise<unknown>;\n      };\n    };\n  }\n}\n\ndeclare module \"@tiptap/core\" {\n  interface Commands<ReturnType> {\n    twitter: {\n      setTwitterEmbed: (tweetId: string) => ReturnType;\n    };\n  }\n}\n\nconst TwitterNodeView = (props: NodeViewProps) => {\n  const { node } = props;\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [loading, setLoading] = useState(true);\n  const [failed, setFailed] = useState(false);\n  const { tweetId } = node.attrs;\n  const renderedTweetIdRef = useRef<string | null>(null);\n  const activeRenderIdRef = useRef(0);\n\n  const retryToken = useRef(0);\n  const [retryCount, setRetryCount] = useState(0);\n\n  useEffect(() => {\n    if (!containerRef.current || !tweetId) {\n      return;\n    }\n\n    if (\n      renderedTweetIdRef.current === tweetId &&\n      containerRef.current.children.length > 0 &&\n      !failed\n    ) {\n      setLoading(false);\n      return;\n    }\n\n    let isCancelled = false;\n    activeRenderIdRef.current += 1;\n    const renderId = activeRenderIdRef.current;\n    setLoading(true);\n    setFailed(false);\n\n    const render = async () => {\n      try {\n        await loadTwitterScript();\n        if (\n          isCancelled ||\n          renderId !== activeRenderIdRef.current ||\n          !containerRef.current\n        ) {\n          return;\n        }\n\n        const { twttr } = window;\n        if (!twttr?.widgets) {\n          throw new Error(\"Twitter widgets script unavailable\");\n        }\n\n        containerRef.current.innerHTML = \"\";\n        const el = await twttr.widgets.createTweet(\n          tweetId,\n          containerRef.current\n        );\n\n        if (\n          isCancelled ||\n          renderId !== activeRenderIdRef.current ||\n          !containerRef.current\n        ) {\n          if (el && typeof (el as HTMLElement).remove === \"function\") {\n            (el as HTMLElement).remove();\n          } else if (containerRef.current) {\n            containerRef.current.innerHTML = \"\";\n          }\n          return;\n        }\n\n        if (!el) {\n          throw new Error(\"Tweet could not be embedded\");\n        }\n\n        // Safety guard: ensure only one tweet element remains in the container\n        while (containerRef.current.children.length > 1) {\n          containerRef.current.firstElementChild?.remove();\n        }\n\n        renderedTweetIdRef.current = tweetId;\n      } catch {\n        if (!isCancelled && renderId === activeRenderIdRef.current) {\n          setFailed(true);\n          renderedTweetIdRef.current = null;\n        }\n      } finally {\n        if (!isCancelled && renderId === activeRenderIdRef.current) {\n          setLoading(false);\n        }\n      }\n    };\n\n    void render();\n\n    return () => {\n      isCancelled = true;\n    };\n  }, [tweetId, retryCount, failed]);\n\n  const handleRetry = useCallback(() => {\n    retryToken.current += 1;\n    setRetryCount(retryToken.current);\n  }, []);\n\n  return (\n    <ResizableNodeView\n      {...props}\n      lockAspect={false}\n      minWidth={300}\n      maxWidth={1200}\n    >\n      {loading && <div className=\"rte-embed-loading\">Loading tweet...</div>}\n      {!loading && failed && (\n        <div className=\"rte-embed-error\">\n          <p>Couldn&apos;t load this tweet.</p>\n          <button type=\"button\" onClick={handleRetry}>\n            Retry\n          </button>\n        </div>\n      )}\n      <div ref={containerRef} />\n    </ResizableNodeView>\n  );\n};\n\nexport const TwitterEmbed = Node.create({\n  ...createAtomBlockMarkdownSpec({\n    allowedAttributes: [\"tweetId\", \"width\", \"height\", \"align\"],\n    nodeName: \"twitter\",\n    parseAttributes: (text) => {\n      const attrs = parseAttributes(text);\n      for (const key of [\"width\", \"height\"]) {\n        if (key in attrs) {\n          attrs[key] = Number(attrs[key]);\n        }\n      }\n      return attrs;\n    },\n    requiredAttributes: [\"tweetId\"],\n  }),\n  addAttributes() {\n    return {\n      align: {\n        default: \"center\",\n        parseHTML: (el: HTMLElement) => el.dataset.align || \"center\",\n        renderHTML: (attrs: Record<string, unknown>) => ({\n          \"data-align\": attrs.align,\n        }),\n      },\n      height: {\n        default: 400,\n        parseHTML: (el: HTMLElement) =>\n          el.dataset.height ? Number(el.dataset.height) : 400,\n        renderHTML: (attrs: Record<string, unknown>) => ({\n          \"data-height\": attrs.height,\n        }),\n      },\n      tweetId: {\n        default: null,\n        parseHTML: (el: HTMLElement) => el.dataset.tweetId,\n        renderHTML: (attrs: Record<string, unknown>) => ({\n          \"data-tweet-id\": attrs.tweetId,\n        }),\n      },\n      width: {\n        default: 550,\n        parseHTML: (el: HTMLElement) =>\n          el.dataset.width ? Number(el.dataset.width) : 550,\n        renderHTML: (attrs: Record<string, unknown>) => ({\n          \"data-width\": attrs.width,\n        }),\n      },\n    };\n  },\n  addCommands() {\n    return {\n      setTwitterEmbed:\n        (tweetId: string) =>\n        ({ commands }) =>\n          commands.insertContent({\n            attrs: { tweetId },\n            type: \"twitter\",\n          }),\n    };\n  },\n  addInputRules() {\n    return [\n      nodeInputRule({\n        find: /https?:\\/\\/(?:www\\.)?(?:twitter\\.com|x\\.com)\\/\\w+\\/status\\/(\\d+)/,\n        getAttributes: (match: RegExpMatchArray) => ({ tweetId: match[1] }),\n        type: this.type,\n      }),\n    ];\n  },\n  addNodeView() {\n    return ReactNodeViewRenderer(TwitterNodeView);\n  },\n  atom: true,\n  draggable: true,\n  group: \"block\",\n  name: \"twitter\",\n  parseHTML() {\n    return [{ tag: 'div[data-type=\"twitter\"]' }];\n  },\n  renderHTML({ node }: { node: { attrs: Record<string, unknown> } }) {\n    return [\n      \"div\",\n      {\n        \"data-align\": node.attrs.align,\n        \"data-height\": node.attrs.height,\n        \"data-tweet-id\": node.attrs.tweetId,\n        \"data-type\": \"twitter\",\n        \"data-width\": node.attrs.width,\n      },\n    ];\n  },\n});\n\nconst extractTweetId = (input: string): string | null => {\n  const clean = input.trim();\n  const match = clean.match(\n    /(?:twitter\\.com|x\\.com)\\/(?:\\w+\\/status\\/|i\\/web\\/status\\/)(\\d+)/\n  );\n  if (match) {\n    return match[1] ?? null;\n  }\n  return /^\\d+$/.test(clean) ? clean : null;\n};\n\nexport const TwitterEmbedControl = ({ className }: { className?: string }) => {\n  const { editor } = useRichTextEditorContext();\n  const [open, setOpen] = useState(false);\n  const [url, setUrl] = useState(\"\");\n  const [touched, setTouched] = useState(false);\n\n  const { active } = useEditorState({\n    editor: editor ?? null,\n    selector: (ctx) => ({\n      active:\n        ctx.editor && !ctx.editor.isDestroyed\n          ? ctx.editor.isActive(\"twitter\")\n          : false,\n    }),\n  }) ?? { active: false };\n\n  const tweetId = url ? extractTweetId(url) : null;\n  const showError = touched && url.length > 0 && !tweetId;\n\n  const resetState = useCallback(() => {\n    setUrl(\"\");\n    setTouched(false);\n  }, []);\n\n  const handleInsert = useCallback(() => {\n    setTouched(true);\n    if (!tweetId || !editor) {\n      return false;\n    }\n    editor.chain().focus().setTwitterEmbed(tweetId).run();\n    resetState();\n    return true;\n  }, [tweetId, editor, resetState]);\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next);\n        if (!next) {\n          resetState();\n        }\n      }}\n    >\n      <DialogTrigger\n        render={\n          <RichTextEditorControl\n            active={active}\n            className={className}\n            title=\"Embed Tweet\"\n            aria-label=\"Embed Tweet\"\n          >\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"currentColor\"\n              className=\"rte-editor-icon\"\n            >\n              <path d=\"M18.2 3.2h3.3l-7.2 8.2 8.5 11.2h-6.6l-5.2-6.8-6 6.8H2.7l7.7-8.8L2.3 3.2h6.8l4.7 6.2 5.4-6.2zm-1.2 17.3h1.8L7 4.8H5.1l11.9 15.7z\" />\n            </svg>\n          </RichTextEditorControl>\n        }\n      />\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Embed Tweet</DialogTitle>\n          <DialogDescription>Paste a tweet URL or ID.</DialogDescription>\n        </DialogHeader>\n        <Input\n          value={url}\n          onChange={(e) => {\n            setUrl(e.target.value);\n            if (touched) {\n              setTouched(false);\n            }\n          }}\n          onBlur={() => setTouched(true)}\n          placeholder=\"https://x.com/user/status/...\"\n          aria-invalid={showError}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" && url && handleInsert()) {\n              setOpen(false);\n            }\n          }}\n        />\n        {showError && (\n          <p className=\"rte-input-error\" role=\"alert\">\n            That doesn&apos;t look like a valid tweet URL or ID.\n          </p>\n        )}\n        <DialogFooter>\n          <DialogClose>Cancel</DialogClose>\n          <button\n            type=\"button\"\n            disabled={!url}\n            className=\"rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50\"\n            onClick={() => {\n              if (handleInsert()) {\n                setOpen(false);\n              }\n            }}\n          >\n            Insert\n          </button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n",
      "path": "editor/controls/rte-twitter-control.tsx",
      "target": "@components/editor/controls/rte-twitter-control.tsx",
      "type": "registry:component"
    },
    {
      "content": "\"use client\";\n\nimport {\n  createAtomBlockMarkdownSpec,\n  Node,\n  nodeInputRule,\n  parseAttributes,\n} from \"@tiptap/core\";\nimport { ReactNodeViewRenderer, useEditorState } from \"@tiptap/react\";\nimport type { NodeViewProps } from \"@tiptap/react\";\nimport { useState, useCallback } from \"react\";\n\nimport { RichTextEditorControl } from \"../controls/rte-control\";\nimport { ResizableNodeView } from \"../extensions/resizable-node-view\";\nimport { useRichTextEditorContext } from \"../rte-context\";\nimport {\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n  DialogClose,\n} from \"../ui/dialog\";\nimport { Input } from \"../ui/input\";\n\ndeclare module \"@tiptap/core\" {\n  interface Commands<ReturnType> {\n    youtube: {\n      setYouTubeEmbed: (src: string) => ReturnType;\n    };\n  }\n}\n\nconst getYouTubeId = (url: string): string | null => {\n  const clean = url.trim();\n  const patterns = [\n    /(?:youtube\\.com\\/watch\\?v=)([a-zA-Z0-9_-]{11})/,\n    /(?:youtu\\.be\\/)([a-zA-Z0-9_-]{11})/,\n    /(?:youtube\\.com\\/embed\\/)([a-zA-Z0-9_-]{11})/,\n    /(?:youtube\\.com\\/shorts\\/)([a-zA-Z0-9_-]{11})/,\n    /(?:youtube\\.com\\/live\\/)([a-zA-Z0-9_-]{11})/,\n    /(?:youtube\\.com\\/v\\/)([a-zA-Z0-9_-]{11})/,\n    /(?:m\\.youtube\\.com\\/watch\\?v=)([a-zA-Z0-9_-]{11})/,\n  ];\n  for (const pattern of patterns) {\n    const match = clean.match(pattern);\n    if (match) {\n      return match[1] ?? null;\n    }\n  }\n  return /^[a-zA-Z0-9_-]{11}$/.test(clean) ? clean : null;\n};\n\nconst YouTubeNodeView = (props: NodeViewProps) => {\n  const { node, updateAttributes } = props;\n  const rawSrc = node.attrs.src || \"\";\n  const videoId = getYouTubeId(rawSrc) || rawSrc;\n  let watchUrl: string | undefined;\n  if (videoId) {\n    watchUrl = videoId.startsWith(\"http\")\n      ? videoId\n      : `https://www.youtube.com/watch?v=${videoId}`;\n  }\n\n  return (\n    <ResizableNodeView\n      {...props}\n      lockAspect\n      aspectRatio={16 / 9}\n      minWidth={320}\n      maxWidth={1200}\n      videoSrc={rawSrc}\n      watchUrl={watchUrl}\n      onSrcChange={(newSrc) => updateAttributes({ src: newSrc })}\n    >\n      <iframe\n        src={`https://www.youtube-nocookie.com/embed/${videoId}`}\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n        allowFullScreen\n        title=\"YouTube video\"\n        className=\"rte-embed-iframe\"\n      />\n    </ResizableNodeView>\n  );\n};\n\nexport const YouTubeEmbed = Node.create({\n  ...createAtomBlockMarkdownSpec({\n    allowedAttributes: [\"src\", \"width\", \"height\", \"align\"],\n    nodeName: \"youtube\",\n    parseAttributes: (text) => {\n      const attrs = parseAttributes(text);\n      for (const key of [\"width\", \"height\"]) {\n        if (key in attrs) {\n          attrs[key] = Number(attrs[key]);\n        }\n      }\n      return attrs;\n    },\n    requiredAttributes: [\"src\"],\n  }),\n  addAttributes() {\n    return {\n      align: {\n        default: \"center\",\n        parseHTML: (el: HTMLElement) => el.dataset.align || \"center\",\n        renderHTML: (attrs: Record<string, unknown>) => ({\n          \"data-align\": attrs.align,\n        }),\n      },\n      height: {\n        default: 315,\n        parseHTML: (el: HTMLElement) =>\n          el.dataset.height ? Number(el.dataset.height) : 315,\n        renderHTML: (attrs: Record<string, unknown>) => ({\n          \"data-height\": attrs.height,\n        }),\n      },\n      src: {\n        default: null,\n        parseHTML: (el: HTMLElement) => el.dataset.src,\n        renderHTML: (attrs: Record<string, unknown>) => ({\n          \"data-src\": attrs.src,\n        }),\n      },\n      width: {\n        default: 560,\n        parseHTML: (el: HTMLElement) =>\n          el.dataset.width ? Number(el.dataset.width) : 560,\n        renderHTML: (attrs: Record<string, unknown>) => ({\n          \"data-width\": attrs.width,\n        }),\n      },\n    };\n  },\n  addCommands() {\n    return {\n      setYouTubeEmbed:\n        (src: string) =>\n        ({ commands }) =>\n          commands.insertContent({\n            attrs: { src },\n            type: \"youtube\",\n          }),\n    };\n  },\n  addInputRules() {\n    return [\n      nodeInputRule({\n        find: /https?:\\/\\/(?:www\\.|m\\.)?(?:youtube\\.com|youtu\\.be)\\/(?:watch\\?v=|embed\\/|shorts\\/|live\\/|v\\/)?([a-zA-Z0-9_-]{11})/,\n        getAttributes: (match: RegExpMatchArray) => ({ src: match[0] }),\n        type: this.type,\n      }),\n    ];\n  },\n  addNodeView() {\n    return ReactNodeViewRenderer(YouTubeNodeView);\n  },\n  atom: true,\n  draggable: true,\n  group: \"block\",\n  name: \"youtube\",\n  parseHTML() {\n    return [{ tag: 'div[data-type=\"youtube\"]' }];\n  },\n  renderHTML({ node }: { node: { attrs: Record<string, unknown> } }) {\n    return [\n      \"div\",\n      {\n        \"data-align\": node.attrs.align,\n        \"data-height\": node.attrs.height,\n        \"data-src\": node.attrs.src,\n        \"data-type\": \"youtube\",\n        \"data-width\": node.attrs.width,\n      },\n    ];\n  },\n});\n\nexport const YouTubeEmbedControl = ({ className }: { className?: string }) => {\n  const { editor } = useRichTextEditorContext();\n  const [open, setOpen] = useState(false);\n  const [url, setUrl] = useState(\"\");\n\n  const { active } = useEditorState({\n    editor: editor ?? null,\n    selector: (ctx) => ({\n      active:\n        ctx.editor && !ctx.editor.isDestroyed\n          ? ctx.editor.isActive(\"youtube\")\n          : false,\n    }),\n  }) ?? { active: false };\n\n  const handleInsert = useCallback(() => {\n    if (url && editor) {\n      editor.chain().focus().setYouTubeEmbed(url).run();\n      setUrl(\"\");\n    }\n  }, [url, editor]);\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger\n        render={\n          <RichTextEditorControl\n            active={active}\n            className={className}\n            title=\"Embed YouTube video\"\n            aria-label=\"Embed YouTube video\"\n          >\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              className=\"rte-editor-icon\"\n            >\n              <path d=\"M23.5 6.2a3 3 0 0 0-2.1-2.1C19.5 3.5 12 3.5 12 3.5s-7.5 0-9.4.6a3 3 0 0 0-2.1 2.1C0 8.1 0 12 0 12s0 3.9.5 5.8a3 3 0 0 0 2.1 2.1c1.9.6 9.4.6 9.4.6s7.5 0 9.4-.6a3 3 0 0 0 2.1-2.1c.5-1.9.5-5.8.5-5.8s0-3.9-.5-5.8z\" />\n              <path d=\"M9.5 15.5V8.5l6.2 3.5z\" />\n            </svg>\n          </RichTextEditorControl>\n        }\n      />\n      <DialogContent showCloseButton={false}>\n        <DialogHeader>\n          <DialogTitle>Embed YouTube video</DialogTitle>\n          <DialogDescription>\n            Paste a YouTube video URL or ID.\n          </DialogDescription>\n        </DialogHeader>\n        <Input\n          value={url}\n          onChange={(e) => setUrl(e.target.value)}\n          placeholder=\"https://youtube.com/watch?v=...\"\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" && url) {\n              handleInsert();\n              setOpen(false);\n            }\n          }}\n        />\n        <DialogFooter>\n          <DialogClose>Cancel</DialogClose>\n          <DialogClose\n            disabled={!url}\n            className=\"rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50\"\n            onClick={handleInsert}\n          >\n            Insert\n          </DialogClose>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n",
      "path": "editor/controls/rte-youtube-control.tsx",
      "target": "@components/editor/controls/rte-youtube-control.tsx",
      "type": "registry:component"
    },
    {
      "content": "import { clsx } from \"clsx\";\nimport type { ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));\n",
      "path": "editor/ui/utils.ts",
      "target": "@components/editor/ui/utils.ts",
      "type": "registry:lib"
    },
    {
      "content": "import { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./utils\";\n\nconst buttonVariants = cva(\n  \"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    defaultVariants: {\n      size: \"default\",\n      variant: \"default\",\n    },\n    variants: {\n      size: {\n        default:\n          \"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        icon: \"size-8\",\n        \"icon-lg\": \"size-9\",\n        \"icon-sm\":\n          \"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg\",\n        \"icon-xs\":\n          \"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3\",\n        lg: \"h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        sm: \"h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n        xs: \"h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n      },\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/80\",\n        destructive:\n          \"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40\",\n        ghost:\n          \"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n        outline:\n          \"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground\",\n      },\n    },\n  }\n);\n\nconst Button = ({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  ...props\n}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) => (\n  <ButtonPrimitive\n    data-slot=\"button\"\n    className={cn(buttonVariants({ className, size, variant }))}\n    {...props}\n  />\n);\n\nexport { Button, buttonVariants };\n",
      "path": "editor/ui/button.tsx",
      "target": "@components/editor/ui/button.tsx",
      "type": "registry:component"
    },
    {
      "content": "\"use client\";\n\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\";\nimport { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./utils\";\n\nconst toggleVariants = cva(\n  \"group/toggle inline-flex items-center justify-center gap-1 rounded-none text-xs font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    defaultVariants: {\n      size: \"default\",\n      variant: \"default\",\n    },\n    variants: {\n      size: {\n        default:\n          \"h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        lg: \"h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        sm: \"h-7 min-w-7 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5\",\n      },\n      variant: {\n        default: \"bg-transparent\",\n        outline: \"border border-input bg-transparent hover:bg-muted\",\n      },\n    },\n  }\n);\n\nconst Toggle = ({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) => (\n  <TogglePrimitive\n    data-slot=\"toggle\"\n    className={cn(toggleVariants({ className, size, variant }))}\n    {...props}\n  />\n);\n\nexport { Toggle, toggleVariants };\n",
      "path": "editor/ui/toggle.tsx",
      "target": "@components/editor/ui/toggle.tsx",
      "type": "registry:component"
    },
    {
      "content": "\"use client\";\n\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport * as React from \"react\";\n\nimport { cn } from \"./utils\";\n\nconst Popover = ({ ...props }: PopoverPrimitive.Root.Props) => (\n  <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n);\n\nconst PopoverTrigger = ({ ...props }: PopoverPrimitive.Trigger.Props) => (\n  <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n);\n\nconst PopoverContent = ({\n  className,\n  align = \"center\",\n  alignOffset = 0,\n  side = \"bottom\",\n  sideOffset = 4,\n  ...props\n}: PopoverPrimitive.Popup.Props &\n  Pick<\n    PopoverPrimitive.Positioner.Props,\n    \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\"\n  >) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Positioner\n      align={align}\n      alignOffset={alignOffset}\n      side={side}\n      sideOffset={sideOffset}\n      className=\"isolate z-50\"\n    >\n      <PopoverPrimitive.Popup\n        data-slot=\"popover-content\"\n        className={cn(\n          \"z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-none bg-popover p-2.5 text-xs text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n          className\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Positioner>\n  </PopoverPrimitive.Portal>\n);\n\nconst PopoverHeader = ({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) => (\n  <div\n    data-slot=\"popover-header\"\n    className={cn(\"flex flex-col gap-1 text-xs\", className)}\n    {...props}\n  />\n);\n\nconst PopoverTitle = ({\n  className,\n  ...props\n}: PopoverPrimitive.Title.Props) => (\n  <PopoverPrimitive.Title\n    data-slot=\"popover-title\"\n    className={cn(\"text-sm font-medium\", className)}\n    {...props}\n  />\n);\n\nconst PopoverDescription = ({\n  className,\n  ...props\n}: PopoverPrimitive.Description.Props) => (\n  <PopoverPrimitive.Description\n    data-slot=\"popover-description\"\n    className={cn(\"text-xs/relaxed text-muted-foreground\", className)}\n    {...props}\n  />\n);\n\nexport {\n  Popover,\n  PopoverContent,\n  PopoverDescription,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverTrigger,\n};\n",
      "path": "editor/ui/popover.tsx",
      "target": "@components/editor/ui/popover.tsx",
      "type": "registry:component"
    },
    {
      "content": "import { Input as InputPrimitive } from \"@base-ui/react/input\";\nimport * as React from \"react\";\n\nimport { cn } from \"./utils\";\n\nconst Input = ({\n  className,\n  type,\n  ...props\n}: React.ComponentProps<\"input\">) => (\n  <InputPrimitive\n    type={type}\n    data-slot=\"input\"\n    className={cn(\n      \"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40\",\n      className\n    )}\n    {...props}\n  />\n);\n\nexport { Input };\n",
      "path": "editor/ui/input.tsx",
      "target": "@components/editor/ui/input.tsx",
      "type": "registry:component"
    },
    {
      "content": "\"use client\";\n\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\n\nimport { cn } from \"./utils\";\n\nconst TooltipProvider = ({\n  delay = 0,\n  ...props\n}: TooltipPrimitive.Provider.Props) => (\n  <TooltipPrimitive.Provider\n    data-slot=\"tooltip-provider\"\n    delay={delay}\n    {...props}\n  />\n);\n\nconst Tooltip = ({ ...props }: TooltipPrimitive.Root.Props) => (\n  <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n);\n\nconst TooltipTrigger = ({ ...props }: TooltipPrimitive.Trigger.Props) => (\n  <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n);\n\nconst TooltipContent = ({\n  className,\n  side = \"top\",\n  sideOffset = 4,\n  align = \"center\",\n  alignOffset = 0,\n  children,\n  ...props\n}: TooltipPrimitive.Popup.Props &\n  Pick<\n    TooltipPrimitive.Positioner.Props,\n    \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\"\n  >) => (\n  <TooltipPrimitive.Portal>\n    <TooltipPrimitive.Positioner\n      align={align}\n      alignOffset={alignOffset}\n      side={side}\n      sideOffset={sideOffset}\n      className=\"isolate z-50\"\n    >\n      <TooltipPrimitive.Popup\n        data-slot=\"tooltip-content\"\n        className={cn(\n          \"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-none bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-none data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-none bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5\" />\n      </TooltipPrimitive.Popup>\n    </TooltipPrimitive.Positioner>\n  </TooltipPrimitive.Portal>\n);\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n",
      "path": "editor/ui/tooltip.tsx",
      "target": "@components/editor/ui/tooltip.tsx",
      "type": "registry:component"
    },
    {
      "content": "\"use client\";\n\nimport { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport * as React from \"react\";\n\nimport { Button } from \"./button\";\nimport { cn } from \"./utils\";\n\nconst Dialog = ({ ...props }: DialogPrimitive.Root.Props) => (\n  <DialogPrimitive.Root data-slot=\"dialog\" {...props} />\n);\n\nconst DialogTrigger = ({ ...props }: DialogPrimitive.Trigger.Props) => (\n  <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n);\n\nconst DialogPortal = ({ ...props }: DialogPrimitive.Portal.Props) => (\n  <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n);\n\nconst DialogClose = ({ ...props }: DialogPrimitive.Close.Props) => (\n  <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n);\n\nconst DialogOverlay = ({\n  className,\n  ...props\n}: DialogPrimitive.Backdrop.Props) => (\n  <DialogPrimitive.Backdrop\n    data-slot=\"dialog-overlay\"\n    className={cn(\n      \"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0\",\n      className\n    )}\n    {...props}\n  />\n);\n\nconst DialogContent = ({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: DialogPrimitive.Popup.Props & {\n  showCloseButton?: boolean;\n}) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Popup\n      data-slot=\"dialog-content\"\n      className={cn(\n        \"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-none bg-popover p-4 text-xs/relaxed text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      {showCloseButton && (\n        <DialogPrimitive.Close\n          data-slot=\"dialog-close\"\n          render={\n            <Button\n              variant=\"ghost\"\n              className=\"absolute top-2 right-2\"\n              size=\"icon-sm\"\n            />\n          }\n        >\n          {\"X\"}\n          <span className=\"sr-only\">Close</span>\n        </DialogPrimitive.Close>\n      )}\n    </DialogPrimitive.Popup>\n  </DialogPortal>\n);\n\nconst DialogHeader = ({ className, ...props }: React.ComponentProps<\"div\">) => (\n  <div\n    data-slot=\"dialog-header\"\n    className={cn(\"flex flex-col gap-1 text-left\", className)}\n    {...props}\n  />\n);\n\nconst DialogFooter = ({\n  className,\n  showCloseButton = false,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showCloseButton?: boolean;\n}) => (\n  <div\n    data-slot=\"dialog-footer\"\n    className={cn(\n      \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    {showCloseButton && (\n      <DialogPrimitive.Close render={<Button variant=\"outline\" />}>\n        Close\n      </DialogPrimitive.Close>\n    )}\n  </div>\n);\n\nconst DialogTitle = ({ className, ...props }: DialogPrimitive.Title.Props) => (\n  <DialogPrimitive.Title\n    data-slot=\"dialog-title\"\n    className={cn(\"text-sm font-medium\", className)}\n    {...props}\n  />\n);\n\nconst DialogDescription = ({\n  className,\n  ...props\n}: DialogPrimitive.Description.Props) => (\n  <DialogPrimitive.Description\n    data-slot=\"dialog-description\"\n    className={cn(\n      \"text-xs/relaxed text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground\",\n      className\n    )}\n    {...props}\n  />\n);\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n};\n",
      "path": "editor/ui/dialog.tsx",
      "target": "@components/editor/ui/dialog.tsx",
      "type": "registry:component"
    },
    {
      "content": "export { Button, buttonVariants } from \"./button\";\nexport { Dialog } from \"./dialog\";\nexport { Input } from \"./input\";\nexport { Popover, PopoverContent, PopoverTrigger } from \"./popover\";\nexport { RteButton } from \"./rte-button\";\nexport type { RteButtonProps } from \"./rte-button\";\nexport { RteButtonGroup } from \"./rte-button-group\";\nexport type { RteButtonGroupProps } from \"./rte-button-group\";\nexport { RteIcon } from \"./rte-icon\";\nexport type { RteIconProps } from \"./rte-icon\";\nexport { RteSeparator } from \"./rte-separator\";\nexport type { RteSeparatorProps } from \"./rte-separator\";\nexport { RteOverlay } from \"./rte-overlay\";\nexport type { RteOverlayProps } from \"./rte-overlay\";\nexport { RteDropdown } from \"./rte-dropdown\";\nexport type { RteDropdownProps } from \"./rte-dropdown\";\nexport { RteColorSwatch } from \"./rte-color-swatch\";\nexport type { RteColorSwatchProps } from \"./rte-color-swatch\";\nexport { RteDropdownItem } from \"./rte-dropdown-item\";\nexport type { RteDropdownItemProps } from \"./rte-dropdown-item\";\nexport { RteDropdownDivider } from \"./rte-dropdown-divider\";\nexport type { RteDropdownDividerProps } from \"./rte-dropdown-divider\";\nexport { RteDropdownIcon } from \"./rte-dropdown-icon\";\nexport type { RteDropdownIconProps } from \"./rte-dropdown-icon\";\nexport { Toggle, toggleVariants } from \"./toggle\";\nexport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"./tooltip\";\nexport { cn } from \"./utils\";\n",
      "path": "editor/ui/index.ts",
      "target": "@components/editor/ui/index.ts",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport interface RteButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  active?: boolean;\n}\n\nconst RteButton = React.forwardRef<HTMLButtonElement, RteButtonProps>(\n  ({ className = \"\", active = false, ...props }, ref) => {\n    const cls = [\n      \"rte-bubble-btn\",\n      active ? \"rte-bubble-btn--active\" : \"\",\n      className,\n    ]\n      .filter(Boolean)\n      .join(\" \");\n\n    return <button ref={ref} type=\"button\" className={cls} {...props} />;\n  }\n);\nRteButton.displayName = \"RteButton\";\n\nexport { RteButton };\n",
      "path": "editor/ui/rte-button.tsx",
      "target": "@components/editor/ui/rte-button.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport type RteButtonGroupProps = React.HTMLAttributes<HTMLDivElement>;\n\nconst RteButtonGroup = React.forwardRef<HTMLDivElement, RteButtonGroupProps>(\n  ({ className = \"\", ...props }, ref) => (\n    <div\n      ref={ref}\n      className={`rte-bubble-group ${className}`.trim()}\n      {...props}\n    />\n  )\n);\nRteButtonGroup.displayName = \"RteButtonGroup\";\n\nexport { RteButtonGroup };\n",
      "path": "editor/ui/rte-button-group.tsx",
      "target": "@components/editor/ui/rte-button-group.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport type RteIconProps = React.HTMLAttributes<HTMLSpanElement>;\n\nconst RteIcon = React.forwardRef<HTMLSpanElement, RteIconProps>(\n  ({ className = \"\", ...props }, ref) => (\n    <span\n      ref={ref}\n      className={`rte-bubble-btn-icon ${className}`.trim()}\n      {...props}\n    />\n  )\n);\nRteIcon.displayName = \"RteIcon\";\n\nexport { RteIcon };\n",
      "path": "editor/ui/rte-icon.tsx",
      "target": "@components/editor/ui/rte-icon.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport type RteSeparatorProps = React.HTMLAttributes<HTMLDivElement>;\n\nconst RteSeparator = React.forwardRef<HTMLDivElement, RteSeparatorProps>(\n  ({ className = \"\", ...props }, ref) => (\n    <div\n      ref={ref}\n      className={`rte-bubble-separator ${className}`.trim()}\n      {...props}\n    />\n  )\n);\nRteSeparator.displayName = \"RteSeparator\";\n\nexport { RteSeparator };\n",
      "path": "editor/ui/rte-separator.tsx",
      "target": "@components/editor/ui/rte-separator.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport type RteOverlayProps = React.HTMLAttributes<HTMLDivElement>;\n\nconst RteOverlay = React.forwardRef<HTMLDivElement, RteOverlayProps>(\n  ({ className = \"\", ...props }, ref) => (\n    <div\n      ref={ref}\n      role=\"presentation\"\n      className={`rte-bubble-overlay ${className}`.trim()}\n      {...props}\n    />\n  )\n);\nRteOverlay.displayName = \"RteOverlay\";\n\nexport { RteOverlay };\n",
      "path": "editor/ui/rte-overlay.tsx",
      "target": "@components/editor/ui/rte-overlay.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\n\nexport interface RteDropdownProps extends React.HTMLAttributes<HTMLDivElement> {\n  variant?: \"default\" | \"language\" | \"color\";\n}\n\nconst variantClass: Record<string, string> = {\n  color: \"rte-bubble-dropdown--color\",\n  default: \"\",\n  language: \"rte-bubble-dropdown--language\",\n};\n\nconst RteDropdown = React.forwardRef<HTMLDivElement, RteDropdownProps>(\n  (\n    { className = \"\", variant = \"default\", children, onMouseLeave, ...props },\n    ref\n  ) => {\n    const containerRef = useRef<HTMLDivElement | null>(null);\n    const [pill, setPill] = useState<{\n      height: number;\n      left: number;\n      top: number;\n      width: number;\n    } | null>(null);\n\n    const handleItemEnter = useCallback((target: EventTarget | null) => {\n      const item = (target as HTMLElement).closest<HTMLElement>(\n        \"[data-dropdown-item]\"\n      );\n      if (!item) {\n        return;\n      }\n      setPill({\n        height: item.offsetHeight,\n        left: item.offsetLeft,\n        top: item.offsetTop,\n        width: item.offsetWidth,\n      });\n    }, []);\n\n    const handleMouseOver = useCallback(\n      (event: React.MouseEvent<HTMLDivElement>) => {\n        handleItemEnter(event.target);\n      },\n      [handleItemEnter]\n    );\n\n    const handleFocus = useCallback(\n      (event: React.FocusEvent<HTMLDivElement>) => {\n        handleItemEnter(event.target);\n      },\n      [handleItemEnter]\n    );\n\n    const handleMouseLeaveFn = useCallback(\n      (event: React.MouseEvent<HTMLDivElement>) => {\n        setPill(null);\n        onMouseLeave?.(event);\n      },\n      [onMouseLeave]\n    );\n\n    const cls = [\"rte-bubble-dropdown\", variantClass[variant], className]\n      .filter(Boolean)\n      .join(\" \");\n\n    return (\n      <div\n        ref={(node) => {\n          containerRef.current = node;\n          if (typeof ref === \"function\") {\n            ref(node);\n          } else if (ref) {\n            ref.current = node;\n          }\n        }}\n        className={cls}\n        onFocus={handleFocus}\n        onMouseLeave={handleMouseLeaveFn}\n        onMouseOver={handleMouseOver}\n        {...props}\n      >\n        <div\n          aria-hidden=\"true\"\n          className={[\n            \"rte-bubble-dropdown-pill\",\n            pill ? \"rte-bubble-dropdown-pill--visible\" : \"\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n          style={\n            pill\n              ? {\n                  height: pill.height,\n                  left: pill.left,\n                  top: pill.top,\n                  width: pill.width,\n                }\n              : undefined\n          }\n        />\n        {children}\n      </div>\n    );\n  }\n);\nRteDropdown.displayName = \"RteDropdown\";\n\nexport { RteDropdown };\n",
      "path": "editor/ui/rte-dropdown.tsx",
      "target": "@components/editor/ui/rte-dropdown.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport interface RteDropdownItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  active?: boolean;\n}\n\nconst RteDropdownItem = React.forwardRef<\n  HTMLButtonElement,\n  RteDropdownItemProps\n>(({ className = \"\", active = false, ...props }, ref) => {\n  const cls = [\n    \"rte-bubble-dropdown-item\",\n    active ? \"rte-bubble-dropdown-item--active\" : \"\",\n    className,\n  ]\n    .filter(Boolean)\n    .join(\" \");\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      data-dropdown-item\n      className={cls}\n      {...props}\n    />\n  );\n});\nRteDropdownItem.displayName = \"RteDropdownItem\";\n\nexport { RteDropdownItem };\n",
      "path": "editor/ui/rte-dropdown-item.tsx",
      "target": "@components/editor/ui/rte-dropdown-item.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport type RteDropdownDividerProps = React.HTMLAttributes<HTMLDivElement>;\n\nconst RteDropdownDivider = React.forwardRef<\n  HTMLDivElement,\n  RteDropdownDividerProps\n>(({ className = \"\", ...props }, ref) => (\n  <div\n    ref={ref}\n    className={`rte-bubble-dropdown-divider ${className}`.trim()}\n    {...props}\n  />\n));\nRteDropdownDivider.displayName = \"RteDropdownDivider\";\n\nexport { RteDropdownDivider };\n",
      "path": "editor/ui/rte-dropdown-divider.tsx",
      "target": "@components/editor/ui/rte-dropdown-divider.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport type RteDropdownIconProps = React.HTMLAttributes<HTMLSpanElement>;\n\nconst RteDropdownIcon = React.forwardRef<HTMLSpanElement, RteDropdownIconProps>(\n  ({ className = \"\", ...props }, ref) => (\n    <span\n      ref={ref}\n      className={`rte-bubble-dropdown-icon ${className}`.trim()}\n      {...props}\n    />\n  )\n);\nRteDropdownIcon.displayName = \"RteDropdownIcon\";\n\nexport { RteDropdownIcon };\n",
      "path": "editor/ui/rte-dropdown-icon.tsx",
      "target": "@components/editor/ui/rte-dropdown-icon.tsx",
      "type": "registry:component"
    },
    {
      "content": "import * as React from \"react\";\n\nexport interface RteColorSwatchProps {\n  color: string;\n  label: string;\n  variant: \"text\" | \"highlight\";\n  active?: boolean;\n  onSelect: (color: string) => void;\n}\n\nconst getCheckColor = (hex: string): string => {\n  const value = hex.replace(\"#\", \"\");\n  const r = Number.parseInt(value.slice(0, 2), 16) / 255;\n  const g = Number.parseInt(value.slice(2, 4), 16) / 255;\n  const b = Number.parseInt(value.slice(4, 6), 16) / 255;\n  const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;\n  return luminance > 0.5 ? \"#18181b\" : \"#ffffff\";\n};\n\nconst RteColorSwatch = React.forwardRef<HTMLButtonElement, RteColorSwatchProps>(\n  ({ color, label, variant, active = false, onSelect }, ref) => {\n    const checkColor = getCheckColor(color);\n\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        aria-pressed={active}\n        aria-label={label}\n        title={label}\n        className={[\n          \"rte-bubble-color-swatch\",\n          variant === \"text\"\n            ? \"rte-bubble-color-swatch--text\"\n            : \"rte-bubble-color-swatch--highlight\",\n          active ? \"rte-bubble-color-swatch--active\" : \"\",\n        ]\n          .filter(Boolean)\n          .join(\" \")}\n        style={\n          {\n            \"--swatch-check\": checkColor,\n            \"--swatch-color\": color,\n          } as React.CSSProperties\n        }\n        onMouseDown={(e) => e.preventDefault()}\n        onClick={() => onSelect(color)}\n      >\n        {variant === \"text\" && !active ? (\n          <span className=\"rte-bubble-color-glyph\" aria-hidden=\"true\">\n            A\n          </span>\n        ) : null}\n        {active ? (\n          <span className=\"rte-bubble-color-check\" aria-hidden=\"true\">\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth={3}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <path d=\"M20 6 9 17l-5-5\" />\n            </svg>\n          </span>\n        ) : null}\n      </button>\n    );\n  }\n);\nRteColorSwatch.displayName = \"RteColorSwatch\";\n\nexport { RteColorSwatch };\n",
      "path": "editor/ui/rte-color-swatch.tsx",
      "target": "@components/editor/ui/rte-color-swatch.tsx",
      "type": "registry:component"
    }
  ],
  "name": "editor",
  "title": "Rich Text Editor",
  "type": "registry:component"
}