"use client";
import { ChevronRight, GripVertical, Loader2, Save } from "lucide-react";
import { AnimatePresence, motion, type Variants } from "motion/react";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useEditorContext } from "./editor-context";
import { RecoveryBanner } from "./recovery-banner";
// Lazy load EditorPane (includes Monaco) - only loaded when drawer opens
const EditorPane = dynamic(
() => import("./editor/editor-pane").then((mod) => mod.EditorPane),
{
loading: () => (
),
ssr: false, // Monaco doesn't work server-side
},
);
import { Button } from "@mdxui/primitives/button";
import { Kbd } from "@mdxui/primitives/kbd";
import { cn } from "@mdxui/primitives/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@mdxui/primitives/tooltip";
import { useIsMobile, usePrefersReducedMotion } from "../hooks/use-media-query";
interface EditorDrawerProps {
isOpen: boolean;
}
const MIN_WIDTH = 320;
const MIN_CONTENT_WIDTH = 375; // Minimum content area when editor is open
const DEFAULT_WIDTH_PERCENT = 1 / 3; // One-third of screen
function getDefaultWidth() {
if (typeof window === "undefined") return 500;
return Math.max(
MIN_WIDTH,
Math.floor(window.innerWidth * DEFAULT_WIDTH_PERCENT),
);
}
export function EditorDrawer({ isOpen }: EditorDrawerProps) {
// Get shared state from context
const { path, isDirty, isSaving, onSave, onClose, recovery } =
useEditorContext();
const prefersReducedMotion = usePrefersReducedMotion();
const isMobile = useIsMobile() ?? false;
const [drawerWidth, setDrawerWidth] = useState(getDefaultWidth);
const [isResizing, setIsResizing] = useState(false);
const drawerRef = useRef(null);
// Handle escape key
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === "Escape" && !isResizing && onClose) {
onClose();
}
},
[onClose, isResizing],
);
useEffect(() => {
if (isOpen) {
document.addEventListener("keydown", handleKeyDown);
}
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, handleKeyDown]);
// Resize handling
const handleResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsResizing(true);
}, []);
useEffect(() => {
if (!isResizing) return;
let rafId: number | null = null;
let latestX = 0;
const handleMouseMove = (e: MouseEvent) => {
latestX = e.clientX;
if (rafId) return; // Skip if RAF pending
rafId = requestAnimationFrame(() => {
const maxWidth = window.innerWidth - MIN_CONTENT_WIDTH;
const newWidth = window.innerWidth - latestX;
setDrawerWidth(Math.max(MIN_WIDTH, Math.min(maxWidth, newWidth)));
rafId = null;
});
};
const handleMouseUp = () => {
setIsResizing(false);
};
document.addEventListener("mousemove", handleMouseMove, { passive: true });
document.addEventListener("mouseup", handleMouseUp);
// Add cursor style to body during resize
document.body.style.cursor = "ew-resize";
document.body.style.userSelect = "none";
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
if (rafId) cancelAnimationFrame(rafId);
};
}, [isResizing]);
// Set CSS custom property for drawer width (used by page content to adjust)
useEffect(() => {
if (typeof window === "undefined") return;
const root = document.documentElement;
if (!isOpen || isMobile) {
// Reset when drawer closes or on mobile (full-screen overlay)
root.style.removeProperty("--editor-drawer-width");
return;
}
root.style.setProperty("--editor-drawer-width", `${drawerWidth}px`);
return () => {
root.style.removeProperty("--editor-drawer-width");
};
}, [isOpen, drawerWidth, isMobile]);
// Memoized animation variants for slide-in from right
const drawerVariants = useMemo(
() => ({
hidden: {
x: "100%",
},
visible: {
x: 0,
transition: {
type: "spring",
damping: 30,
stiffness: 300,
duration: prefersReducedMotion ? 0 : undefined,
},
},
exit: {
x: "100%",
transition: {
type: "spring",
damping: 30,
stiffness: 300,
duration: prefersReducedMotion ? 0 : undefined,
},
},
}),
[prefersReducedMotion],
);
// SSR guard for portal
if (typeof window === "undefined") return null;
return createPortal(
{isOpen && (
{/* Resize handle - hidden on mobile */}
{!isMobile && (
// biome-ignore lint/a11y/noStaticElementInteractions:
)}
{/* Header */}
{/* Close button - left side */}
Close ⌘
I
{/* File path - center */}
{path || "untitled.mdx"}
{/* Save button - right side */}
{onSave && (
Save ⌘
S
)}
{/* Recovery banner - shown when autosaved content is available */}
{recovery && (
)}
{/* Editor content - uses EditorContext for state */}
)}
,
document.body,
);
}