{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "signature-pad",
	"title": "Signature Pad",
	"type": "registry:ui",
	"description": "A pressure and velocity aware drawing canvas that exports to PNG or SVG.",
	"files": [
		{
			"content": "<script lang=\"ts\" module>\r\n\texport type SignaturePoint = { x: number; y: number; w: number };\r\n\texport type SignatureStroke = { color: string; points: SignaturePoint[] };\r\n\r\n\texport type SignatureSegment = {\r\n\t\tx0: number;\r\n\t\ty0: number;\r\n\t\tcx: number;\r\n\t\tcy: number;\r\n\t\tx1: number;\r\n\t\ty1: number;\r\n\t\tw: number;\r\n\t};\r\n\r\n\tconst mid = (a: SignaturePoint, b: SignaturePoint) => ({\r\n\t\tx: (a.x + b.x) / 2,\r\n\t\ty: (a.y + b.y) / 2\r\n\t});\r\n\r\n\t/**\r\n\t * Turns a stroke into quadratic segments (control point = the sampled point,\r\n\t * endpoints = the midpoints of its neighbours). Shared by the canvas renderer\r\n\t * and the SVG exporter so both produce the exact same curve.\r\n\t */\r\n\texport function strokeToSegments(stroke: SignatureStroke): SignatureSegment[] {\r\n\t\tconst p = stroke.points;\r\n\t\tif (p.length < 2) return [];\r\n\r\n\t\tconst segments: SignatureSegment[] = [];\r\n\t\tlet start = { x: p[0].x, y: p[0].y };\r\n\r\n\t\tfor (let i = 1; i < p.length; i++) {\r\n\t\t\tconst end = i === p.length - 1 ? { x: p[i].x, y: p[i].y } : mid(p[i], p[i + 1]);\r\n\t\t\tsegments.push({\r\n\t\t\t\tx0: start.x,\r\n\t\t\t\ty0: start.y,\r\n\t\t\t\tcx: p[i].x,\r\n\t\t\t\tcy: p[i].y,\r\n\t\t\t\tx1: end.x,\r\n\t\t\t\ty1: end.y,\r\n\t\t\t\tw: p[i].w\r\n\t\t\t});\r\n\t\t\tstart = end;\r\n\t\t}\r\n\r\n\t\treturn segments;\r\n\t}\r\n</script>\r\n\r\n<script lang=\"ts\">\r\n\timport { cn } from '$UTILS$';\r\n\timport { onMount } from 'svelte';\r\n\r\n\tlet {\r\n\t\tstrokes = $bindable([]),\r\n\t\tpenColor = 'currentColor',\r\n\t\tbackgroundColor = 'transparent',\r\n\t\tminWidth = 0.7,\r\n\t\tmaxWidth = 2.6,\r\n\t\tvelocityWeight = 0.7,\r\n\t\tdisabled = false,\r\n\t\tclass: className,\r\n\t\tonbegin,\r\n\t\tonend,\r\n\t\tonchange,\r\n\t\t...rest\r\n\t}: {\r\n\t\tstrokes?: SignatureStroke[];\r\n\t\tpenColor?: string;\r\n\t\tbackgroundColor?: string;\r\n\t\tminWidth?: number;\r\n\t\tmaxWidth?: number;\r\n\t\tvelocityWeight?: number;\r\n\t\tdisabled?: boolean;\r\n\t\tclass?: string;\r\n\t\tonbegin?: () => void;\r\n\t\tonend?: (stroke: SignatureStroke) => void;\r\n\t\tonchange?: (strokes: SignatureStroke[]) => void;\r\n\t\t[key: string]: unknown;\r\n\t} = $props();\r\n\r\n\tlet canvas = $state<HTMLCanvasElement | null>(null);\r\n\tlet ctx: CanvasRenderingContext2D | null = null;\r\n\tlet size = $state({ width: 0, height: 0 });\r\n\r\n\tlet drawing = false;\r\n\tlet current: SignatureStroke | null = null;\r\n\tlet lastWidth = maxWidth;\r\n\tlet lastTime = 0;\r\n\r\n\texport const isEmpty = () => strokes.length === 0;\r\n\r\n\tconst colorCache = new Map<string, string>();\r\n\r\n\t/**\r\n\t * Normalises any CSS color to `#rrggbb` / `rgba()`. Themes resolve to modern\r\n\t * color spaces such as `oklch()`, which older SVG viewers do not understand.\r\n\t */\r\n\tfunction normalizeColor(color: string) {\r\n\t\tconst cached = colorCache.get(color);\r\n\t\tif (cached) return cached;\r\n\r\n\t\tconst probe = document.createElement('canvas');\r\n\t\tprobe.width = probe.height = 1;\r\n\t\tconst pctx = probe.getContext('2d', { willReadFrequently: true });\r\n\r\n\t\tlet output = color;\r\n\t\tif (pctx) {\r\n\t\t\tpctx.fillStyle = color;\r\n\t\t\tpctx.fillRect(0, 0, 1, 1);\r\n\t\t\tconst [r, g, b, a] = pctx.getImageData(0, 0, 1, 1).data;\r\n\t\t\tconst hex = (n: number) => n.toString(16).padStart(2, '0');\r\n\t\t\toutput =\r\n\t\t\t\ta === 255\r\n\t\t\t\t\t? `#${hex(r)}${hex(g)}${hex(b)}`\r\n\t\t\t\t\t: `rgba(${r}, ${g}, ${b}, ${Math.round((a / 255) * 1000) / 1000})`;\r\n\t\t}\r\n\r\n\t\tcolorCache.set(color, output);\r\n\t\treturn output;\r\n\t}\r\n\r\n\t/** Resolves `currentColor` against the canvas' inherited text color. */\r\n\tfunction resolvedPen() {\r\n\t\tif (penColor !== 'currentColor') return normalizeColor(penColor);\r\n\t\treturn canvas ? normalizeColor(getComputedStyle(canvas).color) : '#000000';\r\n\t}\r\n\r\n\tfunction paintSegment(c: CanvasRenderingContext2D, seg: SignatureSegment, color: string) {\r\n\t\tc.strokeStyle = color;\r\n\t\tc.lineWidth = seg.w;\r\n\t\tc.lineCap = 'round';\r\n\t\tc.lineJoin = 'round';\r\n\t\tc.beginPath();\r\n\t\tc.moveTo(seg.x0, seg.y0);\r\n\t\tc.quadraticCurveTo(seg.cx, seg.cy, seg.x1, seg.y1);\r\n\t\tc.stroke();\r\n\t}\r\n\r\n\tfunction paintDot(c: CanvasRenderingContext2D, p: SignaturePoint, color: string) {\r\n\t\tc.fillStyle = color;\r\n\t\tc.beginPath();\r\n\t\tc.arc(p.x, p.y, Math.max(p.w, minWidth) / 2, 0, Math.PI * 2);\r\n\t\tc.fill();\r\n\t}\r\n\r\n\tfunction redraw() {\r\n\t\tif (!ctx || !canvas) return;\r\n\r\n\t\tctx.clearRect(0, 0, size.width, size.height);\r\n\r\n\t\tif (backgroundColor !== 'transparent') {\r\n\t\t\tctx.fillStyle = backgroundColor;\r\n\t\t\tctx.fillRect(0, 0, size.width, size.height);\r\n\t\t}\r\n\r\n\t\tfor (const stroke of strokes) {\r\n\t\t\tconst color = stroke.color === 'currentColor' ? resolvedPen() : stroke.color;\r\n\t\t\tif (stroke.points.length === 1) {\r\n\t\t\t\tpaintDot(ctx, stroke.points[0], color);\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\t\t\tfor (const seg of strokeToSegments(stroke)) paintSegment(ctx, seg, color);\r\n\t\t}\r\n\t}\r\n\r\n\tfunction resize() {\r\n\t\tif (!canvas) return;\r\n\r\n\t\tconst rect = canvas.getBoundingClientRect();\r\n\t\tconst ratio = window.devicePixelRatio || 1;\r\n\r\n\t\tsize = { width: rect.width, height: rect.height };\r\n\t\tcanvas.width = Math.round(rect.width * ratio);\r\n\t\tcanvas.height = Math.round(rect.height * ratio);\r\n\r\n\t\tctx = canvas.getContext('2d');\r\n\t\tctx?.scale(ratio, ratio);\r\n\t\tredraw();\r\n\t}\r\n\r\n\t/** Pressure when the device reports it, otherwise a velocity taper. */\r\n\tfunction widthFor(point: { x: number; y: number }, pressure: number, now: number) {\r\n\t\tif (pressure > 0 && pressure !== 0.5) {\r\n\t\t\treturn minWidth + (maxWidth - minWidth) * pressure;\r\n\t\t}\r\n\r\n\t\tconst last = current?.points.at(-1);\r\n\t\tif (!last) return maxWidth;\r\n\r\n\t\tconst dt = Math.max(now - lastTime, 1);\r\n\t\tconst distance = Math.hypot(point.x - last.x, point.y - last.y);\r\n\t\tconst velocity = distance / dt;\r\n\t\tconst target = Math.max(maxWidth - velocity * 4, minWidth);\r\n\r\n\t\treturn velocityWeight * target + (1 - velocityWeight) * lastWidth;\r\n\t}\r\n\r\n\tfunction positionOf(e: PointerEvent) {\r\n\t\tconst rect = canvas!.getBoundingClientRect();\r\n\t\treturn { x: e.clientX - rect.left, y: e.clientY - rect.top };\r\n\t}\r\n\r\n\tfunction addPoint(e: PointerEvent) {\r\n\t\tif (!current || !ctx) return;\r\n\r\n\t\tconst pos = positionOf(e);\r\n\t\tconst now = e.timeStamp;\r\n\t\tconst w = widthFor(pos, e.pressure, now);\r\n\r\n\t\tlastWidth = w;\r\n\t\tlastTime = now;\r\n\t\tcurrent.points.push({ ...pos, w });\r\n\r\n\t\t// Draw only the newest segment so live strokes stay cheap.\r\n\t\tconst segments = strokeToSegments(current);\r\n\t\tconst seg = segments.at(-1);\r\n\t\tif (seg) paintSegment(ctx, seg, resolvedPen());\r\n\t}\r\n\r\n\tfunction handlePointerDown(e: PointerEvent) {\r\n\t\tif (disabled || e.button !== 0) return;\r\n\r\n\t\tcanvas?.setPointerCapture(e.pointerId);\r\n\t\tdrawing = true;\r\n\t\tlastWidth = maxWidth;\r\n\t\tlastTime = e.timeStamp;\r\n\r\n\t\tconst pos = positionOf(e);\r\n\t\tcurrent = { color: penColor, points: [{ ...pos, w: maxWidth }] };\r\n\t\tonbegin?.();\r\n\t}\r\n\r\n\tfunction handlePointerMove(e: PointerEvent) {\r\n\t\tif (!drawing) return;\r\n\t\te.preventDefault();\r\n\r\n\t\tconst events = e.getCoalescedEvents?.() ?? [];\r\n\t\tif (events.length > 0) for (const ev of events) addPoint(ev);\r\n\t\telse addPoint(e);\r\n\t}\r\n\r\n\tfunction handlePointerUp(e: PointerEvent) {\r\n\t\tif (!drawing || !current) return;\r\n\r\n\t\tdrawing = false;\r\n\t\tcanvas?.releasePointerCapture(e.pointerId);\r\n\r\n\t\tif (current.points.length === 1 && ctx) {\r\n\t\t\tpaintDot(ctx, current.points[0], resolvedPen());\r\n\t\t}\r\n\r\n\t\tconst finished = current;\r\n\t\tcurrent = null;\r\n\t\tstrokes = [...strokes, finished];\r\n\r\n\t\tonend?.(finished);\r\n\t\tonchange?.(strokes);\r\n\t}\r\n\r\n\texport function clear() {\r\n\t\tstrokes = [];\r\n\t\tcurrent = null;\r\n\t\tredraw();\r\n\t\tonchange?.(strokes);\r\n\t}\r\n\r\n\texport function undo() {\r\n\t\tif (strokes.length === 0) return;\r\n\t\tstrokes = strokes.slice(0, -1);\r\n\t\tredraw();\r\n\t\tonchange?.(strokes);\r\n\t}\r\n\r\n\texport function toDataURL(type = 'image/png', quality?: number) {\r\n\t\tif (!canvas) return '';\r\n\t\tif (backgroundColor !== 'transparent' || type === 'image/png') {\r\n\t\t\treturn canvas.toDataURL(type, quality);\r\n\t\t}\r\n\r\n\t\t// JPEG has no alpha channel, so flatten onto white first.\r\n\t\tconst flat = document.createElement('canvas');\r\n\t\tflat.width = canvas.width;\r\n\t\tflat.height = canvas.height;\r\n\t\tconst fctx = flat.getContext('2d')!;\r\n\t\tfctx.fillStyle = '#ffffff';\r\n\t\tfctx.fillRect(0, 0, flat.width, flat.height);\r\n\t\tfctx.drawImage(canvas, 0, 0);\r\n\t\treturn flat.toDataURL(type, quality);\r\n\t}\r\n\r\n\texport function toSVG() {\r\n\t\tconst { width, height } = size;\r\n\t\tconst round = (n: number) => Math.round(n * 100) / 100;\r\n\t\tconst parts: string[] = [];\r\n\r\n\t\tif (backgroundColor !== 'transparent') {\r\n\t\t\tparts.push(\r\n\t\t\t\t`<rect width=\"${round(width)}\" height=\"${round(height)}\" fill=\"${normalizeColor(backgroundColor)}\"/>`\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\tfor (const stroke of strokes) {\r\n\t\t\tconst color = stroke.color === 'currentColor' ? resolvedPen() : stroke.color;\r\n\r\n\t\t\tif (stroke.points.length === 1) {\r\n\t\t\t\tconst p = stroke.points[0];\r\n\t\t\t\tparts.push(\r\n\t\t\t\t\t`<circle cx=\"${round(p.x)}\" cy=\"${round(p.y)}\" r=\"${round(Math.max(p.w, minWidth) / 2)}\" fill=\"${color}\"/>`\r\n\t\t\t\t);\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tfor (const seg of strokeToSegments(stroke)) {\r\n\t\t\t\tparts.push(\r\n\t\t\t\t\t`<path d=\"M${round(seg.x0)} ${round(seg.y0)} Q${round(seg.cx)} ${round(seg.cy)} ${round(seg.x1)} ${round(seg.y1)}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"${round(seg.w)}\" stroke-linecap=\"round\"/>`\r\n\t\t\t\t);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${round(width)}\" height=\"${round(height)}\" viewBox=\"0 0 ${round(width)} ${round(height)}\">${parts.join('')}</svg>`;\r\n\t}\r\n\r\n\texport function toSVGDataURL() {\r\n\t\treturn `data:image/svg+xml;charset=utf-8,${encodeURIComponent(toSVG())}`;\r\n\t}\r\n\r\n\tonMount(() => {\r\n\t\tresize();\r\n\r\n\t\tconst observer = new ResizeObserver(resize);\r\n\t\tif (canvas) observer.observe(canvas);\r\n\r\n\t\treturn () => observer.disconnect();\r\n\t});\r\n\r\n\t// Re-render when the stroke list is replaced from the outside.\r\n\t$effect(() => {\r\n\t\tstrokes;\r\n\t\tif (!drawing) redraw();\r\n\t});\r\n</script>\r\n\r\n<canvas\r\n\tbind:this={canvas}\r\n\tonpointerdown={handlePointerDown}\r\n\tonpointermove={handlePointerMove}\r\n\tonpointerup={handlePointerUp}\r\n\tonpointercancel={handlePointerUp}\r\n\tonpointerleave={(e) => drawing && handlePointerUp(e)}\r\n\tclass={cn(\r\n\t\t'h-48 w-full touch-none rounded-md border border-input bg-background',\r\n\t\tdisabled ? 'cursor-not-allowed opacity-60' : 'cursor-crosshair',\r\n\t\tclassName\r\n\t)}\r\n\t{...rest}\r\n></canvas>\r\n",
			"type": "registry:ui",
			"target": "signature-pad/signature-pad.svelte"
		},
		{
			"content": "import SignaturePad from './signature-pad.svelte';\r\n\r\nexport { SignaturePad, SignaturePad as Root };\r\nexport type { SignaturePoint, SignatureStroke } from './signature-pad.svelte';\r\n",
			"type": "registry:ui",
			"target": "signature-pad/index.ts"
		}
	]
}