"use client";
import { useState, useRef, useEffect, useCallback } from "react";
import { PenTool, Undo2, Trash2, Check, Type, Pen } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
type Point = { x: number; y: number };
type Stroke = Point[];
export const title = "React Dialog Block Signature Pad";
export default function DialogSignaturePad() {
const [open, setOpen] = useState(false);
const [strokes, setStrokes] = useState<Stroke[]>([]);
const [currentStroke, setCurrentStroke] = useState<Stroke>([]);
const [isDrawing, setIsDrawing] = useState(false);
const [strokeWidth, setStrokeWidth] = useState(2);
const [typedName, setTypedName] = useState("");
const [activeTab, setActiveTab] = useState("draw");
const [signed, setSigned] = useState(false);
const [signatureDataUrl, setSignatureDataUrl] = useState<string | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const drawCanvas = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = "hsl(var(--foreground))";
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = strokeWidth;
const allStrokes = [...strokes, currentStroke];
allStrokes.forEach((stroke) => {
if (stroke.length < 2) return;
ctx.beginPath();
ctx.moveTo(stroke[0].x, stroke[0].y);
stroke.forEach((point, i) => {
if (i > 0) {
ctx.lineTo(point.x, point.y);
}
});
ctx.stroke();
});
}, [strokes, currentStroke, strokeWidth]);
useEffect(() => {
drawCanvas();
}, [drawCanvas]);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const updateCanvasSize = () => {
const rect = container.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
drawCanvas();
};
updateCanvasSize();
window.addEventListener("resize", updateCanvasSize);
return () => window.removeEventListener("resize", updateCanvasSize);
}, [open, drawCanvas]);
const getCoordinates = (e: React.MouseEvent | React.TouchEvent): Point | null => {
const canvas = canvasRef.current;
if (!canvas) return null;
const rect = canvas.getBoundingClientRect();
if ("touches" in e) {
const touch = e.touches[0];
return {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
};
}
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
};
const handleStart = (e: React.MouseEvent | React.TouchEvent) => {
const point = getCoordinates(e);
if (!point) return;
setIsDrawing(true);
setCurrentStroke([point]);
};
const handleMove = (e: React.MouseEvent | React.TouchEvent) => {
if (!isDrawing) return;
const point = getCoordinates(e);
if (!point) return;
setCurrentStroke((prev) => [...prev, point]);
};
const handleEnd = () => {
if (currentStroke.length > 0) {
setStrokes((prev) => [...prev, currentStroke]);
}
setCurrentStroke([]);
setIsDrawing(false);
};
const handleUndo = () => {
setStrokes((prev) => prev.slice(0, -1));
};
const handleClear = () => {
setStrokes([]);
setCurrentStroke([]);
};
const handleSign = () => {
// Capture the signature as a data URL before showing success
const canvas = canvasRef.current;
if (canvas && activeTab === "draw") {
setSignatureDataUrl(canvas.toDataURL());
}
setSigned(true);
};
const handleClose = () => {
setOpen(false);
setTimeout(() => {
setStrokes([]);
setCurrentStroke([]);
setTypedName("");
setActiveTab("draw");
setSigned(false);
setSignatureDataUrl(null);
}, 200);
};
const hasDrawnSignature = strokes.length > 0;
const hasTypedSignature = typedName.trim().length >= 2;
const canSubmit = activeTab === "draw" ? hasDrawnSignature : hasTypedSignature;
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<PenTool className="mr-2 h-4 w-4" />
Sign Document
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-sm">
{signed ? (
<div className="flex flex-col items-center justify-center py-6 text-center">
<Check className="h-8 w-8 text-primary" />
<h3 className="mt-4 text-lg font-semibold">Signature Captured</h3>
<p className="mt-1 text-sm text-muted-foreground">
Your signature has been recorded.
</p>
<div className="mt-4 w-full rounded-lg border p-3">
{activeTab === "draw" && signatureDataUrl ? (
<img
src={signatureDataUrl}
alt="Your signature"
className="h-16 w-full object-contain"
/>
) : (
<p className="text-2xl py-2" style={{ fontFamily: "cursive" }}>
{typedName}
</p>
)}
</div>
<Button onClick={handleClose} className="mt-6">
Done
</Button>
</div>
) : (
<>
<DialogHeader>
<DialogTitle>Add Your Signature</DialogTitle>
<DialogDescription>
Draw your signature or type your name.
</DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="draw">
<Pen className="mr-2 h-4 w-4" />
Draw
</TabsTrigger>
<TabsTrigger value="type">
<Type className="mr-2 h-4 w-4" />
Type
</TabsTrigger>
</TabsList>
<TabsContent value="draw" className="space-y-3 mt-4">
<div
ref={containerRef}
className="relative h-32 rounded-lg border-2 border-dashed cursor-crosshair touch-none"
>
<canvas
ref={canvasRef}
onMouseDown={handleStart}
onMouseMove={handleMove}
onMouseUp={handleEnd}
onMouseLeave={handleEnd}
onTouchStart={handleStart}
onTouchMove={handleMove}
onTouchEnd={handleEnd}
className="absolute inset-0 w-full h-full"
/>
{!hasDrawnSignature && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<p className="text-sm text-muted-foreground">Sign here</p>
</div>
)}
</div>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 flex-1">
<Label className="text-xs text-muted-foreground">Stroke</Label>
<Slider
value={[strokeWidth]}
onValueChange={([value]) => setStrokeWidth(value)}
min={1}
max={5}
step={0.5}
className="flex-1"
/>
</div>
<div className="flex gap-1">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={handleUndo}
disabled={strokes.length === 0}
>
<Undo2 className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={handleClear}
disabled={strokes.length === 0}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="type" className="space-y-3 mt-4">
<div className="space-y-2">
<Label htmlFor="typed-signature">Full Name</Label>
<Input
id="typed-signature"
placeholder="Enter your name"
value={typedName}
onChange={(e) => setTypedName(e.target.value)}
/>
</div>
<div className="rounded-lg border p-3 h-20 flex items-center justify-center">
{typedName ? (
<p className="text-2xl" style={{ fontFamily: "cursive" }}>
{typedName}
</p>
) : (
<p className="text-sm text-muted-foreground">Preview</p>
)}
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleSign} disabled={!canSubmit}>
Apply
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
);
}