"use client" import { useCallback, useEffect, useRef } from "react" import { computeCropDisplayGeometry, resizeCropRegion, 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: "docs-crop-handle--corner docs-crop-handle--nw cursor-nwse-resize", n: "docs-crop-handle--edge docs-crop-handle--n cursor-ns-resize", ne: "docs-crop-handle--corner docs-crop-handle--ne cursor-nesw-resize", e: "docs-crop-handle--edge docs-crop-handle--e cursor-ew-resize", se: "docs-crop-handle--corner docs-crop-handle--se cursor-nwse-resize", s: "docs-crop-handle--edge docs-crop-handle--s cursor-ns-resize", sw: "docs-crop-handle--corner docs-crop-handle--sw cursor-nesw-resize", w: "docs-crop-handle--edge docs-crop-handle--w cursor-ew-resize", } type CropRegion = Pick function pct(value: number, total: number): string { return `${(value / Math.max(total, 1)) * 100}%` } export function DocsGraphicCropOverlay({ attrs, cropEditBase, frameWidth, frameHeight, imageNaturalWidth, imageNaturalHeight, onChange, onDone, }: { attrs: DocsGraphicAttrs cropEditBase?: Pick | null frameWidth: number frameHeight: number imageNaturalWidth: number imageNaturalHeight: number onChange: (patch: Partial) => void onDone: () => void }) { const rootRef = useRef(null) const { windowRect, cropRect } = computeCropDisplayGeometry( attrs, frameWidth, frameHeight, imageNaturalWidth, imageNaturalHeight, cropEditBase ) const dragRef = useRef<{ handle: Handle startX: number startY: number origin: CropRegion } | null>(null) const readScale = useCallback(() => { const rootRect = rootRef.current?.getBoundingClientRect() return rootRect && windowRect.width > 0 ? rootRect.width / windowRect.width : 1 }, [windowRect.width]) const beginResize = 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 scale = readScale() const dxPx = (event.clientX - startX) / scale const dyPx = (event.clientY - startY) / scale const dxNorm = dxPx / Math.max(windowRect.width, 1) const dyNorm = dyPx / Math.max(windowRect.height, 1) onChange(resizeCropRegion(handle, origin, dxNorm, dyNorm)) } const onUp = () => { dragRef.current = null } window.addEventListener("pointermove", onMove) window.addEventListener("pointerup", onUp) window.addEventListener("pointercancel", onUp) return () => { window.removeEventListener("pointermove", onMove) window.removeEventListener("pointerup", onUp) window.removeEventListener("pointercancel", onUp) } }, [onChange, readScale, windowRect.height, windowRect.width]) useEffect(() => { const onKey = (event: KeyboardEvent) => { if (event.key === "Escape" || event.key === "Enter") { event.preventDefault() onDone() } } window.addEventListener("keydown", onKey, true) return () => window.removeEventListener("keydown", onKey, true) }, [onDone]) const localCropLeft = cropRect.left - windowRect.left const localCropTop = cropRect.top - windowRect.top const cropWidth = pct(cropRect.width, windowRect.width) const cropHeight = pct(cropRect.height, windowRect.height) const cropLeft = pct(localCropLeft, windowRect.width) const cropTop = pct(localCropTop, windowRect.height) const shadeTop = localCropTop const shadeLeft = localCropLeft const shadeRight = windowRect.width - (localCropLeft + cropRect.width) const shadeBottom = windowRect.height - (localCropTop + cropRect.height) return (
{ event.preventDefault() event.stopPropagation() onDone() }} > {shadeTop > 0 ? (
) : null} {shadeBottom > 0 ? (
) : null} {shadeLeft > 0 ? (
) : null} {shadeRight > 0 ? (
) : null}
{HANDLES.map((handle) => ( beginResize(handle, event)} /> ))}
) }