Velocity Signature Pad
A digital signature canvas where the 'ink' stroke weight responds to drawing velocity, simulating a real pen.
Installation
Add this component to your project using the CLI:
npx vui-registry-cli-v1 add velocity-signature-padSource Code
'use client'
import React, { useRef, useState, useEffect, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Eraser, Check, Download, MousePointer2, ShieldCheck, Sparkles } from 'lucide-react'
import { cn } from '@/lib/utils'
interface VelocitySignaturePadProps extends React.HTMLAttributes<HTMLDivElement> {
onSave?: (dataUrl: string) => void
width?: number
height?: number
strokeColor?: string
backgroundColor?: string
}
export function VelocitySignaturePad({
className,
onSave,
width = 600,
height = 300,
strokeColor = '#09090b',
backgroundColor = '#ffffff',
...props
}: VelocitySignaturePadProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [isDrawing, setIsDrawing] = useState(false)
const [hasSignature, setHasSignature] = useState(false)
const [isSaved, setIsSaved] = useState(false)
const [points, setPoints] = useState<{ x: number; y: number; time: number }[]>([])
const minWidth = 1.2
const maxWidth = 3.8
// Initialize Canvas with High DPI and Grid
const initCanvas = useCallback(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const dpr = window.devicePixelRatio || 1
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
// Clear background
ctx.clearRect(0, 0, width, height)
}, [width, height])
useEffect(() => {
initCanvas()
}, [initCanvas])
const getPoint = (e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent) => {
const canvas = canvasRef.current
if (!canvas) return { x: 0, y: 0, time: 0 }
const rect = canvas.getBoundingClientRect()
const clientX = 'touches' in e ? e.touches[0].clientX : (e as MouseEvent).clientX
const clientY = 'touches' in e ? e.touches[0].clientY : (e as MouseEvent).clientY
return {
x: clientX - rect.left,
y: clientY - rect.top,
time: Date.now(),
}
}
const startDrawing = (e: React.MouseEvent | React.TouchEvent) => {
setIsDrawing(true)
setIsSaved(false)
const point = getPoint(e)
setPoints([point])
}
const [inkColor, setInkColor] = useState('#09090b')
useEffect(() => {
const updateInkColor = () => {
const isDark = document.documentElement.classList.contains('dark')
setInkColor(isDark ? '#ffffff' : '#09090b')
}
updateInkColor()
const observer = new MutationObserver(updateInkColor)
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
return () => observer.disconnect()
}, [])
const draw = (e: React.MouseEvent | React.TouchEvent) => {
if (!isDrawing || !canvasRef.current) return
const ctx = canvasRef.current.getContext('2d')
if (!ctx) return
const point = getPoint(e)
const newPoints = [...points, point]
setPoints(newPoints)
if (newPoints.length < 2) return
const lastPoint = newPoints[newPoints.length - 2]
const dist = Math.sqrt(Math.pow(point.x - lastPoint.x, 2) + Math.pow(point.y - lastPoint.y, 2))
const time = point.time - lastPoint.time
const velocity = time > 0 ? dist / time : 0
// Premium ink physics: width reacts to speed
const newWidth = Math.max(minWidth, Math.min(maxWidth, maxWidth - (velocity * 0.5)))
ctx.beginPath()
ctx.moveTo(lastPoint.x, lastPoint.y)
ctx.lineTo(point.x, point.y)
ctx.strokeStyle = inkColor
ctx.lineWidth = newWidth
ctx.stroke()
ctx.closePath()
if (!hasSignature) setHasSignature(true)
}
const stopDrawing = () => {
setIsDrawing(false)
setPoints([])
}
const clearSignature = () => {
initCanvas()
setHasSignature(false)
setIsSaved(false)
}
const handleSave = () => {
if (!canvasRef.current || !hasSignature) return
const dataUrl = canvasRef.current.toDataURL('image/png')
setIsSaved(true)
onSave?.(dataUrl)
}
return (
<div className={cn('flex flex-col gap-3 w-full group/container', className)} {...props}>
<div className="relative overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] dark:border-zinc-800 dark:bg-zinc-950 transition-all duration-500 hover:shadow-[0_20px_50px_rgba(0,0,0,0.1)]">
{/* Subtle Background Grid Pattern */}
<div className="absolute inset-0 pointer-events-none opacity-[0.03] dark:opacity-[0.05]"
style={{ backgroundImage: `radial-gradient(#000 0.5px, transparent 0.5px)`, backgroundSize: '24px 24px' }}
/>
<canvas
ref={canvasRef}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
onTouchStart={startDrawing}
onTouchMove={draw}
onTouchEnd={stopDrawing}
className="relative z-10 cursor-crosshair touch-none"
/>
{/* Animated Placeholder */}
<AnimatePresence>
{!hasSignature && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.1 }}
className="absolute inset-0 z-0 flex flex-col items-center justify-center pointer-events-none"
>
<MousePointer2 className="w-8 h-8 text-zinc-200 dark:text-zinc-800 mb-2 animate-bounce" />
<span className="text-zinc-300 dark:text-zinc-700 text-xl font-medium tracking-tight uppercase">
Draw Signature
</span>
</motion.div>
)}
</AnimatePresence>
{/* Premium Floating Action Bar */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-20">
<AnimatePresence>
{hasSignature && !isSaved && (
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 20, opacity: 0 }}
className="flex items-center gap-1.5 p-1.5 rounded-full bg-white/80 dark:bg-zinc-900/80 backdrop-blur-md border border-zinc-200/50 dark:border-zinc-800/50 shadow-2xl"
>
<ActionButton
onClick={clearSignature}
icon={<Eraser className="w-4 h-4" />}
label="Clear"
variant="danger"
/>
<div className="w-px h-4 bg-zinc-200 dark:bg-zinc-800 mx-1" />
<ActionButton
onClick={handleSave}
icon={<Check className="w-4 h-4" />}
label="Confirm"
variant="primary"
/>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Success Overlay */}
<AnimatePresence>
{isSaved && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute inset-0 z-30 flex items-center justify-center bg-white/60 dark:bg-zinc-950/60 backdrop-blur-[2px]"
>
<motion.div
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
className="flex flex-col items-center"
>
<div className="p-4 rounded-full bg-zinc-900 text-white shadow-xl mb-3">
<ShieldCheck className="w-8 h-8" />
</div>
<p className="font-semibold text-zinc-900 dark:text-zinc-100">Verified</p>
<button
onClick={() => setIsSaved(false)}
className="mt-4 text-xs font-medium text-zinc-500 hover:text-zinc-800 underline underline-offset-4"
>
Edit Signature
</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Enhanced Footer Information */}
<div className="flex justify-between items-center px-1">
<div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest text-zinc-400">
<Sparkles className="w-3 h-3 text-amber-500" />
<span>Velocity Ink Engine</span>
</div>
<div className="flex items-center gap-3">
<span className="text-[10px] text-zinc-400 font-medium tracking-tighter"></span>
<div className="h-1 w-1 rounded-full bg-green-500 animate-pulse" />
</div>
</div>
</div>
)
}
function ActionButton({ onClick, icon, label, variant }: { onClick: () => void, icon: React.ReactNode, label: string, variant: 'danger' | 'primary' }) {
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={onClick}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-full text-xs font-semibold transition-colors shadow-sm",
variant === 'danger'
? "bg-transparent text-zinc-600 hover:bg-red-50 hover:text-red-600 dark:text-zinc-400 dark:hover:bg-red-950/30"
: "bg-zinc-900 text-white hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-white"
)}
>
{icon}
{label}
</motion.button>
)
}
// Demo Component remains similar but with cleaner styling
export default function VelocitySignaturePadDemo() {
return (
<div className="flex items-center justify-center min-h-screen bg-[#fafafa] dark:bg-zinc-950 p-6">
<div className="w-full max-w-xl">
<div className="mb-10 text-center">
<h2 className="text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-50">Authorize Transfer</h2>
<p className="text-zinc-500 mt-2 text-balance">Please provide your handwritten signature to validate this transaction.</p>
</div>
<VelocitySignaturePad />
</div>
</div>
)
}Dependencies
framer-motion: latestlucide-react: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| width | number | - | Width of the canvas in pixels |
| height | number | - | Height of the canvas in pixels |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

