"use client"
import { memo, useRef, type ReactNode } from "react"
import { createPortal } from "react-dom"
import { cn } from "@/lib/utils"
export type DocsRulerDragTooltipState = {
label: string
x: number
y: number
} | null
export const DocsRulerMarginDragTooltip = memo(function DocsRulerMarginDragTooltip({
tooltip,
}: {
tooltip: DocsRulerDragTooltipState
}) {
if (!tooltip || typeof document === "undefined") return null
return createPortal(
{tooltip.label}
,
document.body
)
})
/** Blue downward triangle (margin / left indent). */
export const DocsRulerTriangleMarker = memo(function DocsRulerTriangleMarker({
left,
className,
}: {
left: number
className?: string
}) {
return (
)
})
/** Blue rectangle on horizontal ruler (first-line indent). */
export const DocsRulerFirstLineMarker = memo(function DocsRulerFirstLineMarker({
left,
}: {
left: number
}) {
return (
)
})
/** Blue upward triangle on vertical ruler (top margin). */
export const DocsRulerUpTriangleMarker = memo(function DocsRulerUpTriangleMarker({
top,
}: {
top: number
}) {
return (
)
})
/** Blue downward triangle on vertical ruler (bottom margin). */
export const DocsRulerDownTriangleMarker = memo(function DocsRulerDownTriangleMarker({
top,
}: {
top: number
}) {
return (
)
})
export const DocsRulerDraggableHandle = memo(function DocsRulerDraggableHandle({
style,
className,
disabled,
axis,
ariaLabel,
onPointerDown,
children,
}: {
style: React.CSSProperties
className?: string
disabled?: boolean
axis: "horizontal" | "vertical"
ariaLabel: string
onPointerDown: (event: React.PointerEvent) => void
children: ReactNode
}) {
return (
)
})
export function useRulerPointerDrag({
rulerRef,
axis,
disabled,
onDrag,
onDragEnd,
}: {
rulerRef: React.RefObject
axis: "horizontal" | "vertical"
disabled?: boolean
onDrag: (pagePx: number, clientX: number, clientY: number) => void
onDragEnd: () => void
}) {
const draggingRef = useRef(false)
const onPointerDown = (event: React.PointerEvent) => {
if (disabled) return
event.preventDefault()
event.stopPropagation()
const handle = event.currentTarget
handle.setPointerCapture(event.pointerId)
draggingRef.current = true
document.body.style.userSelect = "none"
document.body.style.cursor = axis === "horizontal" ? "ew-resize" : "ns-resize"
const readPagePx = (clientX: number, clientY: number) => {
const ruler = rulerRef.current
if (!ruler) return null
const rect = ruler.getBoundingClientRect()
const scaleAttr = ruler.dataset.docsRulerScale
const scale = scaleAttr ? Number.parseFloat(scaleAttr) : 1
if (!Number.isFinite(scale) || scale <= 0) return null
return axis === "horizontal"
? (clientX - rect.left) / scale
: (clientY - rect.top) / scale
}
const move = (clientX: number, clientY: number) => {
const pagePx = readPagePx(clientX, clientY)
if (pagePx != null) onDrag(pagePx, clientX, clientY)
}
move(event.clientX, event.clientY)
const onMove = (ev: PointerEvent) => move(ev.clientX, ev.clientY)
const onUp = (ev: PointerEvent) => {
draggingRef.current = false
document.body.style.userSelect = ""
document.body.style.cursor = ""
if (handle.hasPointerCapture(ev.pointerId)) {
handle.releasePointerCapture(ev.pointerId)
}
window.removeEventListener("pointermove", onMove)
window.removeEventListener("pointerup", onUp)
window.removeEventListener("pointercancel", onUp)
onDragEnd()
}
window.addEventListener("pointermove", onMove)
window.addEventListener("pointerup", onUp)
window.addEventListener("pointercancel", onUp)
}
return { onPointerDown, draggingRef }
}