ShadcnTemplate

Modern Rich Text Editor with SHADCN UI

The ShadcnTemplate is a beautiful, production-ready rich text editor that combines the power of Lexical with the elegance of SHADCN UI components.

SHADCN UIFloating ToolbarDraggable BlocksFeature Rich

Interactive Demo

ShadcnTemplate Editor
Try the floating toolbar, context menu (right-click), command palette (Ctrl+K), and drag handles on the left edge of content blocks.
Start typing...

Command Palette

Search for a command to run...

Features

Floating Toolbar

Context-aware toolbar that appears when you select text, providing quick access to formatting options.

Draggable Blocks

Hover over the left edge of content blocks to reveal drag handles for reordering your document.

Context Menu

Right-click anywhere in the editor to access a context menu with formatting and block actions.

Command Palette

Press Ctrl+K to open a searchable command palette for keyboard-driven editing.

Image Dialogs

Beautiful modal dialogs for inserting images via URL or file upload with drag & drop support.

HTML Embeds

Insert and preview HTML content directly in your documents with live editing capabilities.

Installation

Install Required SHADCN UI Components
The ShadcnTemplate requires these SHADCN UI components. Install them all at once:
npx shadcn@latest add button toggle command tooltip tabs select separator label input dropdown-menu switch dialog collapsible textarea
Import Path Note
The code examples below show imports from @/components/ui/.... If your shadcn components are in a different path, adjust the imports accordingly.

Usage Examples

Basic Usage
Get started with the ShadcnTemplate in just a few lines of code.

Basic ShadcnTemplate

import { ShadcnTemplate } from '@lexkit/editor/templates'

function MyEditor() {
  return (
    <ShadcnTemplate
      onReady={(editor) => {
        console.log('Editor ready!')
        // Access editor methods here
        editor.injectMarkdown('# Hello World')
      }}
    />
  )
}

API Reference

ShadcnTemplate Props
className?: string

Additional CSS classes to apply to the editor wrapper.

onReady?: (methods: ShadcnTemplateRef) => void

Callback fired when the editor is ready with access to editor methods.

ShadcnTemplateRef Methods
injectMarkdown(content: string): void

Inject markdown content into the editor.

injectHTML(content: string): void

Inject HTML content into the editor.

getMarkdown(): string

Get the current content as markdown.

getHTML(): string

Get the current content as HTML.

Source Code

The ShadcnTemplate is open source. View the complete implementation on GitHub.

ShadcnTemplate.tsx
The main editor component with all features integrated.

ShadcnTemplate Main Component

"use client";

import React, { useState, useEffect, useMemo, useRef, forwardRef, useCallback, useImperativeHandle } from "react";
import { createPortal } from "react-dom";
import {
  // Core system
  createEditorSystem,

  // Extensions
  boldExtension,
  italicExtension,
  underlineExtension,
  strikethroughExtension,
  linkExtension,
  horizontalRuleExtension,
  TableExtension,
  listExtension,
  historyExtension,
  imageExtension,
  blockFormatExtension,
  htmlExtension,
  MarkdownExtension,
  codeExtension,
  codeFormatExtension,
  HTMLEmbedExtension,
  commandPaletteExtension,
  floatingToolbarExtension,
  contextMenuExtension,
  DraggableBlockExtension,

  // Utilities
  ALL_MARKDOWN_TRANSFORMERS,

  // Types
  type ExtractCommands,
  type ExtractStateQueries,
  type BaseCommands,
} from "@lexkit/editor";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalEditor } from "lexical";
import { Bold, Italic, Underline, Strikethrough, List, ListOrdered, Undo, Redo, Image as ImageIcon, AlignLeft, AlignCenter, AlignRight, Upload, Link as LinkIcon, Unlink, Minus, Code, Terminal, Table as TableIcon, FileCode, Eye, Pencil, Command as CommandIcon, Type, Quote, FileText, Hash, X, CloudUpload, Globe, ChevronDown, Indent, Outdent } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Toggle } from "@/components/ui/toggle";
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut } from "@/components/ui/command";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Switch } from "@/components/ui/switch";
import { Dialog as ShadcnDialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Textarea } from "@/components/ui/textarea";
import {
  commandsToCommandPaletteItems,
  registerKeyboardShortcuts,
} from "./commands";
import { shadcnTheme } from "./theme";
import { cn } from "@/lib/utils";
import "./shadcn-styles.css";

// Editor Mode Types
type EditorMode = "visual" | "html" | "markdown";

// Table Config Type
type TableConfig = {
  rows: number;
  columns: number;
  includeHeaders: boolean;
};

// Ref interface for parent control
export interface ShadcnTemplateRef {
  injectMarkdown: (content: string) => void;
  injectHTML: (content: string) => void;
  getMarkdown: () => string;
  getHTML: () => string;
}

// Custom Shadcn-styled context menu renderer
function ShadcnContextMenuRenderer(props: {
  items: any[];
  position: { x: number; y: number };
  onClose: () => void;
  className: string;
  style?: React.CSSProperties;
  itemClassName: string;
  itemStyle?: React.CSSProperties;
  disabledItemClassName: string;
  disabledItemStyle?: React.CSSProperties;
}) {
  const { items, position, onClose } = props;

  return createPortal(
    <div
      className={cn(
        "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
        props.className
      )}
      style={{
        position: 'fixed',
        left: position.x,
        top: position.y,
        zIndex: 9999,
        ...props.style,
      }}
      onClick={(e) => e.stopPropagation()}
      onContextMenu={(e) => e.preventDefault()}
    >
      {items.map((item: any, index: number) => (
        <div
          key={index}
          className={cn(
            "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
            item.disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
            item.disabled ? props.disabledItemClassName : props.itemClassName
          )}
          style={item.disabled ? props.disabledItemStyle : props.itemStyle}
          onClick={() => {
            if (!item.disabled && item.action) {
              item.action();
              onClose();
            }
          }}
        >
          {item.icon && <item.icon className="mr-2 h-4 w-4" />}
          {item.label}
        </div>
      ))}
    </div>,
    document.body
  );
}

// Create custom Shadcn context menu extension
const shadcnContextMenuExtension = contextMenuExtension.configure({
  defaultRenderer: ShadcnContextMenuRenderer,
  preventDefault: true, // Explicitly prevent default browser context menu
  theme: {
    container: "lexkit-context-menu z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
    item: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground",
    itemDisabled: "opacity-50 cursor-not-allowed pointer-events-none"
  }
});

// Create markdown extension instance for this template
const markdownExt = new MarkdownExtension().configure({
  customTransformers: ALL_MARKDOWN_TRANSFORMERS,
});

// Create table extension instance
const tableExt = new TableExtension().configure({
  enableContextMenu: true,
  contextMenuExtension: shadcnContextMenuExtension,
  markdownExtension: markdownExt,
});

// Extensions array
export const extensions = [
  boldExtension,
  italicExtension,
  underlineExtension,
  strikethroughExtension,
  linkExtension.configure({
    linkSelectedTextOnPaste: true,
    autoLinkText: true,
    autoLinkUrls: true,
  }),
  horizontalRuleExtension,
  tableExt,
  listExtension,
  historyExtension,
  imageExtension,
  blockFormatExtension,
  htmlExtension,
  markdownExt,
  codeExtension,
  codeFormatExtension,
  new HTMLEmbedExtension().configure({
    toggleRenderer: ({ isPreview, onClick, className, style }) => (
      <Button
        variant="outline"
        size="sm"
        onClick={onClick}
        style={style}
      >
        {isPreview ? (
          <>
            <FileCode className="w-4 h-4 mr-2" />
            Edit HTML
          </>
        ) : (
          <>
            <Eye className="w-4 h-4 mr-2" />
            Preview
          </>
        )}
      </Button>
    ),
    markdownExtension: markdownExt,
  }),
  floatingToolbarExtension,
  commandPaletteExtension,
  shadcnContextMenuExtension,
  new DraggableBlockExtension().configure({ // Create fresh instance to avoid caching issues when switching templates
    buttonStackPosition: "right",
  }),
] as const;

// Create a typed editor system for these specific extensions
const { Provider, useEditor } = createEditorSystem<typeof extensions>();

// Extract the types for our specific extensions
type EditorCommands = BaseCommands & ExtractCommands<typeof extensions>;
type EditorStateQueries = ExtractStateQueries<typeof extensions>;
type ExtensionNames = (typeof extensions)[number]["name"];

// Custom hook for image handling
function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null) {
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handlers = useMemo(
    () => ({
      insertImageFromUrl: (url: string, alt = "", caption?: string) => {
        commands.insertImage({ src: url, alt, caption });
      },

      insertImageFromFile: async (file: File, alt = "", caption?: string) => {
        if (imageExtension.config.uploadHandler) {
          try {
            const src = await imageExtension.config.uploadHandler(file);
            commands.insertImage({ src, alt: alt || file.name, caption, file });
          } catch (error) {
            console.error("Failed to upload image:", error);
            // Fallback to object URL
            const src = URL.createObjectURL(file);
            commands.insertImage({ src, alt: alt || file.name, caption, file });
          }
        } else {
          const src = URL.createObjectURL(file);
          commands.insertImage({ src, alt: alt || file.name, caption, file });
        }
      },

      setImageAlignment: (alignment: "left" | "center" | "right" | "none") => {
        commands.setImageAlignment(alignment);
      },

      setImageCaption: (caption: string) => {
        commands.setImageCaption(caption);
      },
    }),
    [commands],
  );

  return { handlers, fileInputRef };
}

// Link Dialog Component
function LinkDialog({
  isOpen,
  onOpenChange,
  initialUrl = "",
  onSubmit,
}: {
  isOpen: boolean;
  onOpenChange: (open: boolean) => void;
  initialUrl?: string;
  onSubmit: (data: { url: string }) => void;
}) {
  const [url, setUrl] = useState(initialUrl);

  useEffect(() => {
    if (isOpen) {
      setUrl(initialUrl);
    }
  }, [isOpen, initialUrl]);

  const handleSubmit = () => {
    if (url.trim()) {
      onSubmit({ url: url.trim() });
      onOpenChange(false);
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === "Enter") {
      e.preventDefault();
      handleSubmit();
    }
  };

  return (
    <ShadcnDialog open={isOpen} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <div className="flex items-center gap-2">
            <LinkIcon className="h-5 w-5" />
            <DialogTitle>Insert Link</DialogTitle>
          </div>
        </DialogHeader>

        <div className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="link-url">URL</Label>
            <Input
              id="link-url"
              type="url"
              placeholder="https://example.com"
              value={url}
              onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUrl(e.target.value)}
              onKeyDown={handleKeyDown}
              autoFocus
            />
          </div>
        </div>

        <DialogFooter>
          <div className="flex justify-end gap-2">
            <Button variant="outline" onClick={() => onOpenChange(false)}>
              Cancel
            </Button>
            <Button onClick={handleSubmit} disabled={!url.trim()}>
              Insert Link
            </Button>
          </div>
        </DialogFooter>
      </DialogContent>
    </ShadcnDialog>
  );
}

// Image Dialog Component
function ImageDialog({
  isOpen,
  onOpenChange,
  onSubmit,
}: {
  isOpen: boolean;
  onOpenChange: (open: boolean) => void;
  onSubmit: (data: {
    activeTab: "upload" | "url";
    url: string;
    alt: string;
    caption: string;
    file: File | null;
  }) => void;
}) {
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [activeTab, setActiveTab] = useState<"upload" | "url">("upload");
  const [url, setUrl] = useState("");
  const [alt, setAlt] = useState("");
  const [caption, setCaption] = useState("");
  const [file, setFile] = useState<File | null>(null);
  const [dragOver, setDragOver] = useState(false);
  const [showAdvanced, setShowAdvanced] = useState(false);

  useEffect(() => {
    if (isOpen) {
      setActiveTab("upload");
      setUrl("");
      setAlt("");
      setCaption("");
      setFile(null);
      setDragOver(false);
      setShowAdvanced(false);
    }
  }, [isOpen]);

  const handleSubmit = () => {
    onSubmit({ activeTab, url, alt, caption, file });
    onOpenChange(false);
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === "Enter" && (activeTab === "url" ? url.trim() : file)) {
      e.preventDefault();
      handleSubmit();
    }
  };

  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault();
    setDragOver(true);
  };

  const handleDragLeave = (e: React.DragEvent) => {
    e.preventDefault();
    setDragOver(false);
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    setDragOver(false);
    const files = e.dataTransfer.files;
    if (files.length > 0 && files[0]) {
      setFile(files[0]);
    }
  };

  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    if (files && files.length > 0 && files[0]) {
      setFile(files[0]);
    }
  };

  const handleRemoveFile = () => {
    setFile(null);
    if (fileInputRef.current) {
      fileInputRef.current.value = "";
    }
  };

  // Check if we have valid content to show advanced options
  const hasValidContent = activeTab === "upload" ? !!file : !!url.trim();

  return (
    <ShadcnDialog open={isOpen} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <div className="flex items-center gap-2">
            <ImageIcon className="h-5 w-5" />
            <DialogTitle>Insert Image</DialogTitle>
          </div>
        </DialogHeader>

        <div className="space-y-4">
          <Tabs value={activeTab} onValueChange={(value: string) => setActiveTab(value as "upload" | "url")}>
            <TabsList className="grid w-full grid-cols-2">
              <TabsTrigger value="upload" className="flex items-center gap-2">
                <Upload className="h-4 w-4" />
                Upload
              </TabsTrigger>
              <TabsTrigger value="url" className="flex items-center gap-2">
                <Globe className="h-4 w-4" />
                URL
              </TabsTrigger>
            </TabsList>

            <TabsContent value="upload" className="space-y-4 mt-4">
              <div className="space-y-2">
                <Label>Upload Image</Label>
                {!file ? (
                  <div
                    className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors cursor-pointer ${
                      dragOver
                        ? "border-primary bg-primary/5"
                        : "border-muted-foreground/25 hover:border-muted-foreground/50"
                    }`}
                    onDragOver={handleDragOver}
                    onDragLeave={handleDragLeave}
                    onDrop={handleDrop}
                    onClick={() => fileInputRef.current?.click()}
                  >
                    <CloudUpload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
                    <p className="text-sm text-muted-foreground mb-2">
                      Drop an image here, or click to select
                    </p>
                    <p className="text-xs text-muted-foreground">
                      Supports: JPG, PNG, GIF, WebP (max 10MB)
                    </p>
                  </div>
                ) : (
                  <div className="border rounded-lg p-4 bg-muted/20">
                    <div className="flex items-center gap-3">
                      <div className="w-16 h-16 rounded border overflow-hidden bg-muted flex-shrink-0">
                        <img
                          src={URL.createObjectURL(file)}
                          alt="Preview"
                          className="w-full h-full object-cover"
                        />
                      </div>
                      <div className="flex-1 min-w-0">
                        <p className="text-sm font-medium truncate">
                          {file.name}
                        </p>
                        <p className="text-xs text-muted-foreground">
                          {(file.size / 1024 / 1024).toFixed(2)} MB
                        </p>
                      </div>
                      <Button
                        type="button"
                        variant="ghost"
                        size="sm"
                        onClick={handleRemoveFile}
                        className="text-muted-foreground hover:text-destructive"
                      >
                        <X className="h-4 w-4" />
                      </Button>
                    </div>
                  </div>
                )}
                <input
                  ref={fileInputRef}
                  type="file"
                  accept="image/*"
                  onChange={handleFileSelect}
                  className="hidden"
                />
              </div>
            </TabsContent>

            <TabsContent value="url" className="space-y-4 mt-4">
              <div className="space-y-2">
                <Label htmlFor="image-url">Image URL</Label>
                <Input
                  id="image-url"
                  type="url"
                  placeholder="https://example.com/image.jpg"
                  value={url}
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUrl(e.target.value)}
                  onKeyDown={handleKeyDown}
                />
              </div>
            </TabsContent>

            {/* Advanced Options - only show when we have valid content */}
            {hasValidContent && (
              <Collapsible
                open={showAdvanced}
                onOpenChange={setShowAdvanced}
                className="mt-4"
              >
                <CollapsibleTrigger asChild>
                  <Button
                    type="button"
                    variant="ghost"
                    className="w-full justify-between p-2 h-auto"
                  >
                    <span className="text-sm font-medium">Advanced Options</span>
                    <ChevronDown
                      className={`h-4 w-4 transition-transform ${
                        showAdvanced ? "rotate-180" : ""
                      }`}
                    />
                  </Button>
                </CollapsibleTrigger>
                <CollapsibleContent className="space-y-4">
                  <div className="space-y-2">
                    <Label htmlFor="image-alt">Alt Text (optional)</Label>
                    <Input
                      id="image-alt"
                      placeholder="Describe the image for accessibility"
                      value={alt}
                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAlt(e.target.value)}
                    />
                  </div>

                  <div className="space-y-2">
                    <Label htmlFor="image-caption">Caption (optional)</Label>
                    <Input
                      id="image-caption"
                      placeholder="Image caption"
                      value={caption}
                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCaption(e.target.value)}
                    />
                  </div>
                </CollapsibleContent>
              </Collapsible>
            )}
          </Tabs>
        </div>

        <DialogFooter>
          <div className="flex justify-end gap-2">
            <Button variant="outline" onClick={() => onOpenChange(false)}>
              Cancel
            </Button>
            <Button
              onClick={handleSubmit}
              disabled={activeTab === "upload" ? !file : !url.trim()}
            >
              Insert Image
            </Button>
          </div>
        </DialogFooter>
      </DialogContent>
    </ShadcnDialog>
  );
}

// Floating Toolbar Component
function FloatingToolbarRenderer({
  openLinkDialog,
}: {
  openLinkDialog: (options?: { initialUrl?: string }) => void;
}) {
  const { commands, activeStates, extensions, hasExtension } = useEditor();
  const [isVisible, setIsVisible] = useState(false);
  const [selectionRect, setSelectionRect] = useState<any>(null);

  const floatingExtension = extensions.find(
    (ext) => ext.name === "floatingToolbar",
  ) as any;

  useEffect(() => {
    if (!floatingExtension) return;

    const checkState = () => {
      const visible = floatingExtension.getIsVisible();
      const rect = floatingExtension.getSelectionRect();
      setIsVisible(visible);
      setSelectionRect(rect);
    };

    const interval = setInterval(checkState, 200);
    return () => clearInterval(interval);
  }, [floatingExtension]);

  if (!isVisible || !selectionRect) return null;

  const isImageSelected = activeStates.imageSelected;

  return createPortal(
    <TooltipProvider>
      <div
        className="flex items-center gap-1 p-2 bg-background border border-border rounded-lg shadow-lg"
        style={{
          position: "absolute",
          top: selectionRect.y,
          ...(selectionRect.positionFromRight
            ? { right: 10, left: "auto" }
            : { left: selectionRect.x, right: "auto" }),
          zIndex: 50,
          pointerEvents: "auto",
        }}
      >
        {isImageSelected ? (
          <>
            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.isImageAlignedLeft ? "pressed" : "default"}
                  pressed={activeStates.isImageAlignedLeft}
                  onPressedChange={() => commands.setImageAlignment("left")}
                >
                  <AlignLeft className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Align Left</TooltipContent>
            </Tooltip>

            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.isImageAlignedCenter ? "pressed" : "default"}
                  pressed={activeStates.isImageAlignedCenter}
                  onPressedChange={() => commands.setImageAlignment("center")}
                >
                  <AlignCenter className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Align Center</TooltipContent>
            </Tooltip>

            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.isImageAlignedRight ? "pressed" : "default"}
                  pressed={activeStates.isImageAlignedRight}
                  onPressedChange={() => commands.setImageAlignment("right")}
                >
                  <AlignRight className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Align Right</TooltipContent>
            </Tooltip>

            <Separator orientation="vertical" className="h-6" />

            <Tooltip>
              <TooltipTrigger asChild>
                <Button
                  size="sm"
                  variant="ghost"
                  onClick={() => {
                    const caption = prompt("Enter caption:") || "";
                    commands.setImageCaption(caption);
                  }}
                >
                  <Type className="h-4 w-4" />
                </Button>
              </TooltipTrigger>
              <TooltipContent>Edit Caption</TooltipContent>
            </Tooltip>
          </>
        ) : (
          <>
            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.bold ? "pressed" : "default"}
                  pressed={activeStates.bold}
                  onPressedChange={() => commands.toggleBold()}
                >
                  <Bold className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Bold</TooltipContent>
            </Tooltip>

            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.italic ? "pressed" : "default"}
                  pressed={activeStates.italic}
                  onPressedChange={() => commands.toggleItalic()}
                >
                  <Italic className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Italic</TooltipContent>
            </Tooltip>

            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.underline ? "pressed" : "default"}
                  pressed={activeStates.underline}
                  onPressedChange={() => commands.toggleUnderline()}
                >
                  <Underline className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Underline</TooltipContent>
            </Tooltip>

            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.strikethrough ? "pressed" : "default"}
                  pressed={activeStates.strikethrough}
                  onPressedChange={() => commands.toggleStrikethrough()}
                >
                  <Strikethrough className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Strikethrough</TooltipContent>
            </Tooltip>

            <Separator orientation="vertical" className="h-6" />

            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.code ? "pressed" : "default"}
                  pressed={activeStates.code}
                  onPressedChange={() => commands.formatText("code")}
                >
                  <Code className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Inline Code</TooltipContent>
            </Tooltip>

            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.isLink ? "pressed" : "default"}
                  pressed={activeStates.isLink}
                  disabled={!activeStates.isTextSelected && !activeStates.isLink}
                  onPressedChange={() => {
                    if (activeStates.isLink) {
                      commands.removeLink();
                    } else if (activeStates.isTextSelected) {
                      openLinkDialog({});
                    }
                  }}
                >
                  {activeStates.isLink ? (
                    <Unlink className="h-4 w-4" />
                  ) : (
                    <LinkIcon className="h-4 w-4" />
                  )}
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>
                {activeStates.isLink ? "Remove Link" : "Add Link"}
              </TooltipContent>
            </Tooltip>

            <Separator orientation="vertical" className="h-6" />

            {hasExtension("list") && (
              <>
                <Tooltip>
                  <TooltipTrigger asChild>
                    <Toggle
                      size="sm"
                      pressed={activeStates.unorderedList}
                      onPressedChange={() => commands.toggleUnorderedList()}
                    >
                      <List className="h-4 w-4" />
                    </Toggle>
                  </TooltipTrigger>
                  <TooltipContent>Bullet List</TooltipContent>
                </Tooltip>

                <Tooltip>
                  <TooltipTrigger asChild>
                    <Toggle
                      size="sm"
                      pressed={activeStates.orderedList}
                      onPressedChange={() => commands.toggleOrderedList()}
                    >
                      <ListOrdered className="h-4 w-4" />
                    </Toggle>
                  </TooltipTrigger>
                  <TooltipContent>Numbered List</TooltipContent>
                </Tooltip>
              </>
            )}
          </>
        )}
      </div>
    </TooltipProvider>,
    document.body
  );
}

// Toolbar Component
function Toolbar({
  commands,
  hasExtension,
  activeStates,
  onCommandPaletteOpen,
  openLinkDialog,
  openImageDialog,
}: {
  commands: EditorCommands;
  hasExtension: (name: ExtensionNames) => boolean;
  activeStates: EditorStateQueries;
  onCommandPaletteOpen: () => void;
  openLinkDialog: (options?: { initialUrl?: string }) => void;
  openImageDialog: () => void;
}) {
  const [showTableDialog, setShowTableDialog] = useState(false);
  const [tableConfig, setTableConfig] = useState<TableConfig>({
    rows: 3,
    columns: 3,
    includeHeaders: false,
  });

  const blockFormatOptions = [
    { value: "p", label: "Paragraph", icon: <FileText className="h-4 w-4" /> },
    { value: "h1", label: "Heading 1", icon: <Hash className="h-4 w-4" /> },
    { value: "h2", label: "Heading 2", icon: <Hash className="h-4 w-4" /> },
    { value: "h3", label: "Heading 3", icon: <Hash className="h-4 w-4" /> },
    { value: "quote", label: "Quote", icon: <Quote className="h-4 w-4" /> },
  ];

  const currentBlockFormat = activeStates.isH1
    ? "h1"
    : activeStates.isH2
      ? "h2"
      : activeStates.isH3
        ? "h3"
        : activeStates.isQuote
          ? "quote"
          : "p";

  const handleBlockFormatChange = (value: string) => {
    if (value === "p") commands.toggleParagraph();
    else if (value.startsWith("h")) commands.toggleHeading(value as "h1" | "h2" | "h3");
    else if (value === "quote") commands.toggleQuote();
  };

  return (
    <TooltipProvider>
      <div className="flex flex-wrap items-center gap-2 p-2 bg-transparent">
        {/* Text Formatting Section */}
        <div className="flex items-center gap-1">
          <Tooltip>
            <TooltipTrigger asChild>
              <Toggle
                size="sm"
                variant={activeStates.bold ? "pressed" : "default"}
                pressed={activeStates.bold}
                onPressedChange={() => commands.toggleBold()}
              >
                <Bold className="h-4 w-4" />
              </Toggle>
            </TooltipTrigger>
            <TooltipContent>Bold (Ctrl+B)</TooltipContent>
          </Tooltip>

          <Tooltip>
            <TooltipTrigger asChild>
              <Toggle
                size="sm"
                variant={activeStates.italic ? "pressed" : "default"}
                pressed={activeStates.italic}
                onPressedChange={() => commands.toggleItalic()}
              >
                <Italic className="h-4 w-4" />
              </Toggle>
            </TooltipTrigger>
            <TooltipContent>Italic (Ctrl+I)</TooltipContent>
          </Tooltip>

          <Tooltip>
            <TooltipTrigger asChild>
              <Toggle
                size="sm"
                variant={activeStates.underline ? "pressed" : "default"}
                pressed={activeStates.underline}
                onPressedChange={() => commands.toggleUnderline()}
              >
                <Underline className="h-4 w-4" />
              </Toggle>
            </TooltipTrigger>
            <TooltipContent>Underline (Ctrl+U)</TooltipContent>
          </Tooltip>

          <Tooltip>
            <TooltipTrigger asChild>
              <Toggle
                size="sm"
                variant={activeStates.strikethrough ? "pressed" : "default"}
                pressed={activeStates.strikethrough}
                onPressedChange={() => commands.toggleStrikethrough()}
              >
                <Strikethrough className="h-4 w-4" />
              </Toggle>
            </TooltipTrigger>
            <TooltipContent>Strikethrough</TooltipContent>
          </Tooltip>

          <Tooltip>
            <TooltipTrigger asChild>
              <Toggle
                size="sm"
                variant={activeStates.code ? "pressed" : "default"}
                pressed={activeStates.code}
                onPressedChange={() => commands.formatText("code")}
              >
                <Code className="h-4 w-4" />
              </Toggle>
            </TooltipTrigger>
            <TooltipContent>Inline Code</TooltipContent>
          </Tooltip>

          <Tooltip>
            <TooltipTrigger asChild>
              <Toggle
                size="sm"
                variant={activeStates.isLink ? "pressed" : "default"}
                pressed={activeStates.isLink}
                disabled={!activeStates.isTextSelected && !activeStates.isLink}
                onPressedChange={() => {
                  if (activeStates.isLink) {
                    commands.removeLink();
                  } else {
                    openLinkDialog({});
                  }
                }}
              >
                {activeStates.isLink ? (
                  <Unlink className="h-4 w-4" />
                ) : (
                  <LinkIcon className="h-4 w-4" />
                )}
              </Toggle>
            </TooltipTrigger>
            <TooltipContent>
              {activeStates.isLink ? "Remove Link" : "Insert Link"}
            </TooltipContent>
          </Tooltip>
        </div>

        <Separator orientation="vertical" className="h-6" />

        {/* Block Format Section */}
        {hasExtension("blockFormat") && (
          <div className="flex items-center gap-1">
            <Select
              value={currentBlockFormat}
              onValueChange={handleBlockFormatChange}
            >
              <Tooltip>
                <TooltipTrigger asChild>
                  <SelectTrigger className="w-32 h-8">
                    <SelectValue />
                  </SelectTrigger>
                </TooltipTrigger>
                <TooltipContent>Text Format</TooltipContent>
              </Tooltip>
              <SelectContent>
                {blockFormatOptions.map((option) => (
                  <SelectItem key={option.value} value={option.value}>
                    <div className="flex items-center gap-2">
                      {option.icon}
                      {option.label}
                    </div>
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
        )}

        <Separator orientation="vertical" className="h-6" />

        {/* Lists Section */}
        {hasExtension("list") && (
          <div className="flex items-center gap-1">
            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.unorderedList ? "pressed" : "default"}
                  pressed={activeStates.unorderedList}
                  onPressedChange={() => commands.toggleUnorderedList()}
                >
                  <List className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Bullet List</TooltipContent>
            </Tooltip>

            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.orderedList ? "pressed" : "default"}
                  pressed={activeStates.orderedList}
                  onPressedChange={() => commands.toggleOrderedList()}
                >
                  <ListOrdered className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>Numbered List</TooltipContent>
            </Tooltip>

            {(activeStates.unorderedList || activeStates.orderedList) && (
              <>
                <Tooltip>
                  <TooltipTrigger asChild>
                    <Button
                      size="sm"
                      variant="ghost"
                      onClick={() => commands.indentList()}
                    >
                      <Indent className="h-4 w-4" />
                    </Button>
                  </TooltipTrigger>
                  <TooltipContent>Indent List</TooltipContent>
                </Tooltip>

                <Tooltip>
                  <TooltipTrigger asChild>
                    <Button
                      size="sm"
                      variant="ghost"
                      onClick={() => commands.outdentList()}
                    >
                      <Outdent className="h-4 w-4" />
                    </Button>
                  </TooltipTrigger>
                  <TooltipContent>Outdent List</TooltipContent>
                </Tooltip>
              </>
            )}
          </div>
        )}

        <Separator orientation="vertical" className="h-6" />

        {/* Media & Content Section */}
        <div className="flex items-center gap-1">
          {/* Image Insert */}
          {hasExtension("image") && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Button size="sm" variant="ghost" onClick={openImageDialog}>
                  <ImageIcon className="h-4 w-4" />
                </Button>
              </TooltipTrigger>
              <TooltipContent>Insert Image</TooltipContent>
            </Tooltip>
          )}

          {/* Table Insert */}
          {hasExtension("table") && (
            <>
              <Tooltip>
                <TooltipTrigger asChild>
                  <Button size="sm" variant="ghost" onClick={() => setShowTableDialog(true)}>
                    <TableIcon className="h-4 w-4" />
                  </Button>
                </TooltipTrigger>
                <TooltipContent>Insert Table</TooltipContent>
              </Tooltip>

              <ShadcnDialog open={showTableDialog} onOpenChange={setShowTableDialog}>
                <DialogContent className="sm:max-w-md">
                  <DialogHeader>
                    <DialogTitle>Insert Table</DialogTitle>
                  </DialogHeader>
                  <div className="space-y-4">
                    <div className="grid grid-cols-2 gap-4">
                      <div className="space-y-2">
                        <Label htmlFor="rows">Rows</Label>
                        <Input
                          id="rows"
                          type="number"
                          min="1"
                          max="20"
                          value={tableConfig.rows}
                          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                            setTableConfig((prev: TableConfig) => ({
                              ...prev,
                              rows: parseInt(e.target.value) || 1,
                            }))
                          }
                          onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
                            if (e.key === "Enter") {
                              e.preventDefault();
                              commands.insertTable(tableConfig);
                              setShowTableDialog(false);
                            }
                          }}
                        />
                      </div>
                      <div className="space-y-2">
                        <Label htmlFor="columns">Columns</Label>
                        <Input
                          id="columns"
                          type="number"
                          min="1"
                          max="20"
                          value={tableConfig.columns}
                          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                            setTableConfig((prev: TableConfig) => ({
                              ...prev,
                              columns: parseInt(e.target.value) || 1,
                            }))
                          }
                          onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
                            if (e.key === "Enter") {
                              e.preventDefault();
                              commands.insertTable(tableConfig);
                              setShowTableDialog(false);
                            }
                          }}
                        />
                      </div>
                    </div>
                    <div className="flex items-center space-x-2">
                      <Switch
                        id="headers"
                        checked={tableConfig.includeHeaders || false}
                        onCheckedChange={(checked: boolean) =>
                          setTableConfig((prev: TableConfig) => ({
                            ...prev,
                            includeHeaders: checked,
                          }))
                        }
                      />
                      <Label htmlFor="headers">Include headers</Label>
                    </div>
                  </div>
                  <DialogFooter>
                    <div className="flex justify-end space-x-2">
                      <Button
                        variant="outline"
                        onClick={() => setShowTableDialog(false)}
                      >
                        Cancel
                      </Button>
                      <Button
                        onClick={() => {
                          commands.insertTable(tableConfig);
                          setShowTableDialog(false);
                        }}
                      >
                        Insert Table
                      </Button>
                    </div>
                  </DialogFooter>
                </DialogContent>
              </ShadcnDialog>
            </>
          )}

          {/* Horizontal Rule */}
          {hasExtension("horizontalRule") && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Button
                  size="sm"
                  variant="ghost"
                  onClick={() => commands.insertHorizontalRule()}
                >
                  <Minus className="h-4 w-4" />
                </Button>
              </TooltipTrigger>
              <TooltipContent>Insert Horizontal Rule</TooltipContent>
            </Tooltip>
          )}

          {/* HTML Embed */}
          {hasExtension("htmlEmbed") && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Toggle
                  size="sm"
                  variant={activeStates.isHTMLEmbedSelected ? "pressed" : "default"}
                  pressed={activeStates.isHTMLEmbedSelected}
                  onPressedChange={() => commands.insertHTMLEmbed()}
                >
                  <FileCode className="h-4 w-4" />
                </Toggle>
              </TooltipTrigger>
              <TooltipContent>HTML Embed</TooltipContent>
            </Tooltip>
          )}
        </div>

        <Separator orientation="vertical" className="h-6" />

        {/* History Section */}
        {hasExtension("history") && (
          <div className="flex items-center gap-1">
            <Tooltip>
              <TooltipTrigger asChild>
                <Button
                  size="sm"
                  variant="ghost"
                  disabled={!activeStates.canUndo}
                  onClick={() => commands.undo()}
                >
                  <Undo className="h-4 w-4" />
                </Button>
              </TooltipTrigger>
              <TooltipContent>Undo (Ctrl+Z)</TooltipContent>
            </Tooltip>

            <Tooltip>
              <TooltipTrigger asChild>
                <Button
                  size="sm"
                  variant="ghost"
                  disabled={!activeStates.canRedo}
                  onClick={() => commands.redo()}
                >
                  <Redo className="h-4 w-4" />
                </Button>
              </TooltipTrigger>
              <TooltipContent>Redo (Ctrl+Y)</TooltipContent>
            </Tooltip>
          </div>
        )}

        <Separator orientation="vertical" className="h-6" />

        {/* Utility Section */}
        <div className="flex items-center gap-1">
          <Tooltip>
            <TooltipTrigger asChild>
              <Button size="sm" variant="ghost" onClick={onCommandPaletteOpen}>
                <CommandIcon className="h-4 w-4" />
              </Button>
            </TooltipTrigger>
            <TooltipContent>Command Palette (Ctrl+K)</TooltipContent>
          </Tooltip>
        </div>
      </div>
    </TooltipProvider>
  );
}

// Mode Tabs Component
function ModeTabs({
  mode,
  onModeChange,
}: {
  mode: EditorMode;
  onModeChange: (mode: EditorMode) => void;
}) {
  return (
    <Tabs
      value={mode}
      onValueChange={(value: string) => onModeChange(value as EditorMode)}
    >
      <TabsList className="grid w-full max-w-md grid-cols-3 bg-muted/50">
        <TabsTrigger value="visual" className="flex items-center gap-2 text-sm">
          <Eye className="h-4 w-4" />
          Visual
        </TabsTrigger>
        <TabsTrigger value="html" className="flex items-center gap-2 text-sm">
          <FileCode className="h-4 w-4" />
          HTML
        </TabsTrigger>
        <TabsTrigger
          value="markdown"
          className="flex items-center gap-2 text-sm"
        >
          <FileText className="h-4 w-4" />
          Markdown
        </TabsTrigger>
      </TabsList>
    </Tabs>
  );
}

// HTML Source View Component
function HTMLSourceView({
  htmlContent,
  onHtmlChange,
}: {
  htmlContent: string;
  onHtmlChange: (html: string) => void;
}) {
  return (
    <Textarea
      className={shadcnTheme.sourceView?.textarea || "w-full h-full min-h-[600px] p-4 bg-background border-none rounded-none font-mono text-sm resize-none focus:outline-none focus:ring-0"}
      value={htmlContent}
      onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onHtmlChange(e.target.value)}
      placeholder="Enter HTML content..."
      spellCheck={false}
    />
  );
}

// Markdown Source View Component
function MarkdownSourceView({
  markdownContent,
  onMarkdownChange,
}: {
  markdownContent: string;
  onMarkdownChange: (markdown: string) => void;
}) {
  return (
    <Textarea
      className={shadcnTheme.sourceView?.textarea || "w-full h-full min-h-[600px] p-4 bg-background border-none rounded-none font-mono text-sm resize-none focus:outline-none focus:ring-0"}
      value={markdownContent}
      onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onMarkdownChange(e.target.value)}
      placeholder="Enter Markdown content..."
      spellCheck={false}
    />
  );
}

// Error Boundary Component
function ErrorBoundary({ children }: { children: React.ReactNode }) {
  return <>{children}</>;
}

// Main Editor Content Component
function EditorContent({
  className,
  onReady,
}: {
  className?: string;
  onReady?: (methods: ShadcnTemplateRef) => void;
}) {
  const { commands, hasExtension, activeStates, lexical: editor } = useEditor();
  const [mode, setMode] = useState<EditorMode>("visual");
  const [content, setContent] = useState({ html: "", markdown: "" });
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
  const [linkDialogOpen, setLinkDialogOpen] = useState(false);
  const [imageDialogOpen, setImageDialogOpen] = useState(false);
  const [linkInitial, setLinkInitial] = useState({ url: "" });
  const commandsRef = useRef<EditorCommands>(commands);

  // Store onReady in ref to avoid infinite loops
  const onReadyRef = useRef(onReady);
  const readyCalledRef = useRef(false);

  useEffect(() => {
    onReadyRef.current = onReady;
  }, [onReady]);

  useEffect(() => {
    commandsRef.current = commands;
  }, [commands]);

  const methods = useMemo<ShadcnTemplateRef>(
    () => ({
      injectMarkdown: (content: string) => {
        if (editor) {
          editor.update(() => {
            commandsRef.current.importFromMarkdown(content, { immediate: true });
          });
        }
      },
      injectHTML: (content: string) => {
        if (editor) {
          editor.update(() => {
            commandsRef.current.importFromHTML(content);
          });
        }
      },
      getMarkdown: () => commandsRef.current.exportToMarkdown(),
      getHTML: () => commandsRef.current.exportToHTML(),
    }),
    [], // No dependencies to prevent recreation
  );

  const { handlers: imageHandlers } = useImageHandlers(commands, editor);

  const openLinkDialog = useCallback(
    (options: { initialUrl?: string } = {}) => {
      const { initialUrl = "" } = options;
      setLinkInitial({ url: initialUrl });
      setLinkDialogOpen(true);
    },
    [],
  );

  const handleLinkSubmit = useCallback(
    ({ url }: { url: string }) => {
      commands.insertLink(url);
    },
    [commands],
  );

  const handleImageSubmit = useCallback(
    ({ activeTab, url, alt, caption, file }: {
      activeTab: "upload" | "url";
      url: string;
      alt: string;
      caption: string;
      file: File | null;
    }) => {
      if (activeTab === "upload" && file) {
        imageHandlers.insertImageFromFile(file, alt, caption);
      } else if (activeTab === "url" && url.trim()) {
        imageHandlers.insertImageFromUrl(url.trim(), alt, caption);
      }
    },
    [imageHandlers],
  );

  useEffect(() => {
    if (!editor || !commands || readyCalledRef.current) return;

    const paletteCommands = commandsToCommandPaletteItems(commands);
    paletteCommands.forEach((cmd) => commands.registerCommand(cmd));

    const originalShowCommand = commands.showCommandPalette;
    (commands as any).showCommandPalette = () => setCommandPaletteOpen(true);

    const unregisterShortcuts = registerKeyboardShortcuts(commands, document.body);

    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.ctrlKey || e.metaKey) && e.key === "k") {
        e.preventDefault();
        setCommandPaletteOpen(true);
      }
    };
    document.addEventListener("keydown", handleKeyDown);

    // Call onReady only once
    readyCalledRef.current = true;
    onReadyRef.current?.(methods);

    return () => {
      unregisterShortcuts();
      document.removeEventListener("keydown", handleKeyDown);
      (commands as any).showCommandPalette = originalShowCommand;
    };
  }, [editor, commands, methods]); // Only depend on editor and commands

  const handleModeChange = (newMode: EditorMode) => {
    if (mode === "markdown" && newMode !== "markdown" && hasExtension("markdown")) {
      commands.importFromMarkdown(content.markdown, { immediate: true });
    }
    if (mode === "html" && newMode !== "html" && hasExtension("html")) {
      commands.importFromHTML(content.html);
    }
    if (newMode === "markdown" && mode !== "markdown" && hasExtension("markdown")) {
      setContent((prev) => ({ ...prev, markdown: commands.exportToMarkdown() }));
    }
    if (newMode === "html" && mode !== "html" && hasExtension("html")) {
      setContent((prev) => ({ ...prev, html: commands.exportToHTML() }));
    }
    setMode(newMode);
    if (newMode === "visual") {
      setTimeout(() => editor?.focus(), 100);
    }
  };

  const handleHtmlChange = (html: string) => setContent((prev) => ({ ...prev, html }));

  const handleMarkdownChange = (markdown: string) => setContent((prev) => ({ ...prev, markdown }));

  return (
    <div className="flex flex-col min-h-[500px]">
      {/* Mode Tabs at top */}
      <div className="px-4 py-3 border-b border-border">
        <ModeTabs mode={mode} onModeChange={handleModeChange} />
      </div>

      {/* Sticky Toolbar Header - only show in visual mode */}
      {mode === "visual" && (
        <div className="z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b border-border">
          <div className="px-4 py-3">
            <Toolbar
              commands={commands}
              hasExtension={hasExtension}
              activeStates={activeStates}
              onCommandPaletteOpen={() => setCommandPaletteOpen(true)}
              openLinkDialog={openLinkDialog}
              openImageDialog={() => setImageDialogOpen(true)}
            />
          </div>
        </div>
      )}

      <div className="relative ">
        {/* Editor Content - Always render RichText but control visibility */}
        <div 
          className="min-h-[600px] prose prose-lg max-w-none p-4"
          style={{ display: mode === "visual" ? "block" : "none" }}
        >
          <RichTextPlugin
            contentEditable={<ContentEditable className={shadcnTheme.contentEditable} />}
            placeholder={<div className={shadcnTheme.placeholder}>Start typing...</div>}
            ErrorBoundary={ErrorBoundary}
          />
          <FloatingToolbarRenderer openLinkDialog={openLinkDialog} />
        </div>

        {mode === "html" && (
          <HTMLSourceView
            htmlContent={content.html}
            onHtmlChange={handleHtmlChange}
          />
        )}

        {mode === "markdown" && (
          <MarkdownSourceView
            markdownContent={content.markdown}
            onMarkdownChange={handleMarkdownChange}
          />
        )}
      </div>

      {/* Dialogs */}
      <LinkDialog
        isOpen={linkDialogOpen}
        onOpenChange={setLinkDialogOpen}
        initialUrl={linkInitial.url}
        onSubmit={handleLinkSubmit}
      />

      <ImageDialog
        isOpen={imageDialogOpen}
        onOpenChange={setImageDialogOpen}
        onSubmit={handleImageSubmit}
      />

      {/* Command Palette */}
      <CommandDialog open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen}>
        <CommandInput placeholder="Type a command or search..." />
        <CommandList>
          <CommandEmpty>No results found.</CommandEmpty>
          {Object.entries(
            commandsToCommandPaletteItems(commands).reduce(
              (groups, cmd) => {
                const category = cmd.category || "Other";
                if (!groups[category]) groups[category] = [];
                groups[category].push(cmd);
                return groups;
              },
              {} as Record<string, ReturnType<typeof commandsToCommandPaletteItems>>,
            ),
          ).map(([category, categoryCommands]) => (
            <CommandGroup key={category} heading={category}>
              {categoryCommands.map((cmd: any) => (
                <CommandItem
                  key={cmd.id}
                  onSelect={() => {
                    cmd.action();
                    setCommandPaletteOpen(false);
                  }}
                >
                  {cmd.label}
                  {cmd.shortcut && <CommandShortcut>{cmd.shortcut}</CommandShortcut>}
                </CommandItem>
              ))}
            </CommandGroup>
          ))}
        </CommandList>
      </CommandDialog>
    </div>
  );
}

// Main Template Component
interface ShadcnTemplateProps {
  className?: string;
  onReady?: (methods: ShadcnTemplateRef) => void;
}

export const ShadcnTemplate = forwardRef<ShadcnTemplateRef, ShadcnTemplateProps>(
  ({ className, onReady }, ref) => {
    const [editorMethods, setEditorMethods] = useState<ShadcnTemplateRef | null>(null);

    // Configure image extension
    useEffect(() => {
      imageExtension.configure({
        uploadHandler: async (file: File) => {
          const objectUrl = URL.createObjectURL(file);
          return objectUrl;
        },
        defaultAlignment: "center",
        resizable: true,
        pasteListener: { insert: true, replace: true },
        debug: false,
      });
    }, []);

    // Handle when editor is ready
    const handleEditorReady = useCallback(
      (methods: ShadcnTemplateRef) => {
        setEditorMethods(methods);
        onReady?.(methods);
      },
      [onReady],
    );

    // Expose methods via ref
    useImperativeHandle(
      ref,
      () => ({
        injectMarkdown: (content: string) => editorMethods?.injectMarkdown(content),
        injectHTML: (content: string) => editorMethods?.injectHTML(content),
        getMarkdown: () => editorMethods?.getMarkdown() || "",
        getHTML: () => editorMethods?.getHTML() || "",
      }),
      [editorMethods],
    );

    return (
      <div className={`shadcn-editor-wrapper ${className || ""}`}>
        <Provider extensions={extensions} config={{ theme: shadcnTheme }}>
          <EditorContent className={className} onReady={handleEditorReady} />
        </Provider>
      </div>
    );
  },
);

ShadcnTemplate.displayName = "ShadcnTemplate";