"use client" import { useCallback, useEffect, useRef } from "react" import type { DocsGraphicAttrs } from "@/lib/drive/docs-graphic-types" import { cn } from "@/lib/utils" const HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"] as const type Handle = (typeof HANDLES)[number] const HANDLE_CLASS: Record = { nw: "left-0 top-0 cursor-nwse-resize", n: "left-1/2 top-0 -translate-x-1/2 cursor-ns-resize", ne: "right-0 top-0 cursor-nesw-resize", e: "right-0 top-1/2 -translate-y-1/2 cursor-ew-resize", se: "right-0 bottom-0 cursor-nwse-resize", s: "left-1/2 bottom-0 -translate-x-1/2 cursor-ns-resize", sw: "left-0 bottom-0 cursor-nesw-resize", w: "left-0 top-1/2 -translate-y-1/2 cursor-ew-resize", } function clamp01(v: number): number { return Math.min(1, Math.max(0, v)) } function resizeCrop( handle: Handle, crop: Pick, dxNorm: number, dyNorm: number ): Pick { let { cropX, cropY, cropWidth, cropHeight } = crop if (handle.includes("e")) cropWidth = clamp01(cropWidth + dxNorm) if (handle.includes("w")) { cropWidth = clamp01(cropWidth - dxNorm) cropX = clamp01(cropX + dxNorm) } if (handle.includes("s")) cropHeight = clamp01(cropHeight + dyNorm) if (handle.includes("n")) { cropHeight = clamp01(cropHeight - dyNorm) cropY = clamp01(cropY + dyNorm) } cropWidth = Math.max(0.05, cropWidth) cropHeight = Math.max(0.05, cropHeight) return { cropX, cropY, cropWidth, cropHeight } } export function DocsGraphicCropOverlay({ attrs, frameWidth, frameHeight, onChange, onDone, }: { attrs: DocsGraphicAttrs frameWidth: number frameHeight: number onChange: (patch: Partial) => void onDone: () => void }) { const dragRef = useRef<{ handle: Handle startX: number startY: number origin: Pick } | null>(null) const onHandleDown = useCallback( (handle: Handle, event: React.PointerEvent) => { event.preventDefault() event.stopPropagation() ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId) dragRef.current = { handle, startX: event.clientX, startY: event.clientY, origin: { cropX: attrs.cropX, cropY: attrs.cropY, cropWidth: attrs.cropWidth, cropHeight: attrs.cropHeight, }, } }, [attrs.cropHeight, attrs.cropWidth, attrs.cropX, attrs.cropY] ) useEffect(() => { const onMove = (event: PointerEvent) => { if (!dragRef.current) return const { handle, startX, startY, origin } = dragRef.current const dxNorm = (event.clientX - startX) / Math.max(frameWidth, 1) const dyNorm = (event.clientY - startY) / Math.max(frameHeight, 1) onChange(resizeCrop(handle, origin, dxNorm, dyNorm)) } const onUp = () => { dragRef.current = null } window.addEventListener("pointermove", onMove) window.addEventListener("pointerup", onUp) return () => { window.removeEventListener("pointermove", onMove) window.removeEventListener("pointerup", onUp) } }, [frameHeight, frameWidth, onChange]) useEffect(() => { const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onDone() } window.addEventListener("keydown", onKey) return () => window.removeEventListener("keydown", onKey) }, [onDone]) const left = `${attrs.cropX * 100}%` const top = `${attrs.cropY * 100}%` const width = `${attrs.cropWidth * 100}%` const height = `${attrs.cropHeight * 100}%` return (
{HANDLES.map((handle) => ( onHandleDown(handle, event)} /> ))}
) }