import { useCallback, useEffect, useRef, type ComponentProps, } from "react" import { Button } from "~/components/ui/button" import { cn } from "~/lib/utils" type SignaturePadProps = Omit, "onChange"> & { width?: number height?: number strokeColor?: string strokeWidth?: number disabled?: boolean onChange?: (dataUrl: string | null) => void onClear?: () => void } function SignaturePad({ width = 400, height = 160, strokeColor, strokeWidth = 2, disabled = false, onChange, onClear, className, ...props }: SignaturePadProps) { const canvasRef = useRef(null) const drawing = useRef(false) const empty = useRef(true) const getCtx = useCallback(() => { const canvas = canvasRef.current if (!canvas) return null const ctx = canvas.getContext("2d") if (!ctx) return null ctx.strokeStyle = strokeColor ?? "currentColor" ctx.lineWidth = strokeWidth ctx.lineCap = "round" ctx.lineJoin = "round" return ctx }, [strokeColor, strokeWidth]) const getPos = (e: MouseEvent | TouchEvent, canvas: HTMLCanvasElement) => { const rect = canvas.getBoundingClientRect() const scaleX = canvas.width / rect.width const scaleY = canvas.height / rect.height const source = "touches" in e ? e.touches[0] : e return { x: (source.clientX - rect.left) * scaleX, y: (source.clientY - rect.top) * scaleY, } } const startDraw = useCallback( (e: MouseEvent | TouchEvent) => { if (disabled) return const canvas = canvasRef.current if (!canvas) return const ctx = getCtx() if (!ctx) return drawing.current = true const { x, y } = getPos(e, canvas) ctx.beginPath() ctx.moveTo(x, y) e.preventDefault() }, [disabled, getCtx] ) const draw = useCallback( (e: MouseEvent | TouchEvent) => { if (!drawing.current || disabled) return const canvas = canvasRef.current if (!canvas) return const ctx = getCtx() if (!ctx) return const { x, y } = getPos(e, canvas) ctx.lineTo(x, y) ctx.stroke() empty.current = false e.preventDefault() }, [disabled, getCtx] ) const endDraw = useCallback(() => { if (!drawing.current) return drawing.current = false const canvas = canvasRef.current if (!canvas) return onChange?.(empty.current ? null : canvas.toDataURL()) }, [onChange]) useEffect(() => { const canvas = canvasRef.current if (!canvas) return canvas.addEventListener("mousedown", startDraw) canvas.addEventListener("mousemove", draw) canvas.addEventListener("mouseup", endDraw) canvas.addEventListener("mouseleave", endDraw) canvas.addEventListener("touchstart", startDraw, { passive: false }) canvas.addEventListener("touchmove", draw, { passive: false }) canvas.addEventListener("touchend", endDraw) return () => { canvas.removeEventListener("mousedown", startDraw) canvas.removeEventListener("mousemove", draw) canvas.removeEventListener("mouseup", endDraw) canvas.removeEventListener("mouseleave", endDraw) canvas.removeEventListener("touchstart", startDraw) canvas.removeEventListener("touchmove", draw) canvas.removeEventListener("touchend", endDraw) } }, [startDraw, draw, endDraw]) const clear = () => { const canvas = canvasRef.current if (!canvas) return const ctx = canvas.getContext("2d") ctx?.clearRect(0, 0, canvas.width, canvas.height) empty.current = true onChange?.(null) onClear?.() } return (
Sign above
) } export { SignaturePad } export type { SignaturePadProps }