{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature Pad",
  "description": "Dialog-based signature capture with hold-to-confirm interaction.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dialog"
  ],
  "files": [
    {
      "path": "components/ui/signature-pad.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { useEffect, useState, useRef, useCallback } from 'react'\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\n// Progress is now inline in the button\nimport { cn } from '@/lib/utils'\nimport { Eraser, Pen, Check, Pencil, RotateCcw } from 'lucide-react'\n\ntype SignaturePadProps = {\n  value?: string | null\n  onChange: (signature: string | null) => void\n  disabled?: boolean\n  className?: string\n  holdToSignDuration?: number // Duration in ms to hold for confirming signature\n}\n\nconst CANVAS_WIDTH = 400\nconst CANVAS_HEIGHT = 200\nconst DEFAULT_HOLD_DURATION = 1500 // 1.5 seconds\n\nconst disableTouchScroll = (canvas: HTMLCanvasElement) => {\n  const preventScroll = (e: TouchEvent) => {\n    e.preventDefault()\n  }\n\n  canvas.addEventListener('touchstart', preventScroll, { passive: false })\n  canvas.addEventListener('touchmove', preventScroll, { passive: false })\n  canvas.addEventListener('touchend', preventScroll, { passive: false })\n\n  return () => {\n    canvas.removeEventListener('touchstart', preventScroll)\n    canvas.removeEventListener('touchmove', preventScroll)\n    canvas.removeEventListener('touchend', preventScroll)\n  }\n}\n\nexport default function SignaturePad({\n  value,\n  onChange,\n  disabled = false,\n  className,\n  holdToSignDuration = DEFAULT_HOLD_DURATION,\n}: SignaturePadProps) {\n  const [isDialogOpen, setIsDialogOpen] = useState(false)\n  const [isDrawing, setIsDrawing] = useState(false)\n  const [lastPosition, setLastPosition] = useState<{\n    x: number\n    y: number\n  } | null>(null)\n  const [hasDrawn, setHasDrawn] = useState(false)\n  const [holdProgress, setHoldProgress] = useState(0)\n  const [isHolding, setIsHolding] = useState(false)\n\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const holdTimerRef = useRef<NodeJS.Timeout | null>(null)\n  const holdStartRef = useRef<number | null>(null)\n  const animationFrameRef = useRef<number | null>(null)\n\n  // Helper function to get the correct stroke color based on theme\n  const getStrokeColor = () => {\n    const isDarkClass = document.documentElement.classList.contains('dark')\n    const isLightClass = document.documentElement.classList.contains('light')\n    const systemPrefersDark = window.matchMedia(\n      '(prefers-color-scheme: dark)',\n    ).matches\n\n    const isDarkMode = isDarkClass || (!isLightClass && systemPrefersDark)\n    return isDarkMode ? '#ffffff' : '#000000'\n  }\n\n  // Setup canvas when dialog opens\n  useEffect(() => {\n    if (!isDialogOpen) return\n\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const ctx = canvas.getContext('2d')\n    if (ctx) {\n      ctx.lineWidth = 2\n      ctx.lineCap = 'round'\n      ctx.lineJoin = 'round'\n      ctx.strokeStyle = getStrokeColor()\n    }\n\n    const updateStrokeColor = () => {\n      const ctx = canvas.getContext('2d')\n      if (!ctx) return\n      ctx.strokeStyle = getStrokeColor()\n    }\n\n    const cleanupTouchScroll = disableTouchScroll(canvas)\n\n    const observer = new MutationObserver(updateStrokeColor)\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: ['class'],\n    })\n\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n    mediaQuery.addEventListener('change', updateStrokeColor)\n\n    return () => {\n      cleanupTouchScroll()\n      observer.disconnect()\n      mediaQuery.removeEventListener('change', updateStrokeColor)\n    }\n  }, [isDialogOpen])\n\n  // Reset canvas state when dialog opens\n  useEffect(() => {\n    if (isDialogOpen) {\n      setHasDrawn(false)\n      setHoldProgress(0)\n      setIsHolding(false)\n      // Clear canvas on open\n      const canvas = canvasRef.current\n      const ctx = canvas?.getContext('2d')\n      if (canvas && ctx) {\n        ctx.clearRect(0, 0, canvas.width, canvas.height)\n      }\n    }\n  }, [isDialogOpen])\n\n  const startDrawing = (\n    e:\n      | React.MouseEvent<HTMLCanvasElement>\n      | React.TouchEvent<HTMLCanvasElement>,\n  ) => {\n    e.preventDefault()\n    setIsDrawing(true)\n    setHasDrawn(true)\n    draw(e)\n  }\n\n  const stopDrawing = () => {\n    if (!isDrawing) return\n    setIsDrawing(false)\n    setLastPosition(null)\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext('2d')\n    if (canvas && ctx) {\n      ctx.beginPath()\n    }\n  }\n\n  const draw = (\n    e:\n      | React.MouseEvent<HTMLCanvasElement>\n      | React.TouchEvent<HTMLCanvasElement>,\n  ) => {\n    e.preventDefault()\n    if (!isDrawing) return\n\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext('2d')\n    if (canvas && ctx) {\n      // Ensure correct stroke color before drawing\n      ctx.strokeStyle = getStrokeColor()\n      \n      const rect = canvas.getBoundingClientRect()\n      const scaleX = canvas.width / rect.width\n      const scaleY = canvas.height / rect.height\n      const x =\n        (('touches' in e ? e.touches[0].clientX : e.clientX) - rect.left) *\n        scaleX\n      const y =\n        (('touches' in e ? e.touches[0].clientY : e.clientY) - rect.top) *\n        scaleY\n\n      if (lastPosition) {\n        const midX = (lastPosition.x + x) / 2\n        const midY = (lastPosition.y + y) / 2\n\n        ctx.beginPath()\n        ctx.moveTo(lastPosition.x, lastPosition.y)\n        ctx.quadraticCurveTo(midX, midY, x, y)\n        ctx.stroke()\n      } else {\n        ctx.beginPath()\n        ctx.moveTo(x, y)\n      }\n\n      setLastPosition({ x, y })\n    }\n  }\n\n  const clearSignature = () => {\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext('2d')\n    if (canvas && ctx) {\n      ctx.clearRect(0, 0, canvas.width, canvas.height)\n      setHasDrawn(false)\n      setHoldProgress(0)\n    }\n  }\n\n  const updateHoldProgress = useCallback(() => {\n    if (!holdStartRef.current) return\n\n    const elapsed = Date.now() - holdStartRef.current\n    const progress = Math.min((elapsed / holdToSignDuration) * 100, 100)\n    setHoldProgress(progress)\n\n    if (progress < 100) {\n      animationFrameRef.current = requestAnimationFrame(updateHoldProgress)\n    }\n  }, [holdToSignDuration])\n\n  const handleHoldStart = useCallback(\n    (e: React.PointerEvent) => {\n      if (!hasDrawn) return\n\n      e.preventDefault()\n      setIsHolding(true)\n      holdStartRef.current = Date.now()\n      setHoldProgress(0)\n\n      // Start progress animation\n      animationFrameRef.current = requestAnimationFrame(updateHoldProgress)\n\n      // Set timer for completion\n      holdTimerRef.current = setTimeout(() => {\n        const canvas = canvasRef.current\n        if (canvas) {\n          const dataUrl = canvas.toDataURL('image/png')\n          onChange(dataUrl)\n          setIsDialogOpen(false)\n        }\n        setIsHolding(false)\n        setHoldProgress(0)\n      }, holdToSignDuration)\n    },\n    [hasDrawn, holdToSignDuration, onChange, updateHoldProgress],\n  )\n\n  const handleHoldEnd = useCallback(() => {\n    setIsHolding(false)\n    holdStartRef.current = null\n\n    if (holdTimerRef.current) {\n      clearTimeout(holdTimerRef.current)\n      holdTimerRef.current = null\n    }\n\n    if (animationFrameRef.current) {\n      cancelAnimationFrame(animationFrameRef.current)\n      animationFrameRef.current = null\n    }\n\n    setHoldProgress(0)\n  }, [])\n\n  const handleClearValue = () => {\n    onChange(null)\n  }\n\n  const handleReset = () => {\n    onChange(null)\n    setIsDialogOpen(false)\n  }\n\n  const handleEditSignature = () => {\n    setIsDialogOpen(true)\n  }\n\n  // Cleanup timers on unmount\n  useEffect(() => {\n    return () => {\n      if (holdTimerRef.current) {\n        clearTimeout(holdTimerRef.current)\n      }\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n    }\n  }, [])\n\n  // If we have a signature value, show the signature image\n  if (value) {\n    return (\n      <div\n        className={cn(\n          'relative inline-flex items-center justify-start mt-2',\n          className,\n        )}\n      >\n        <div className=\"relative border border-input rounded-md overflow-hidden bg-white dark:bg-zinc-900\">\n          {/* eslint-disable-next-line @next/next/no-img-element */}\n          <img\n            src={value}\n            alt=\"Signature\"\n            className=\"w-[200px] h-[100px] object-contain\"\n          />\n          <div className=\"absolute bottom-1 right-1 flex gap-1\">\n            <Button\n              type=\"button\"\n              size=\"icon\"\n              variant=\"outline\"\n              className=\"h-7 w-7 rounded-full bg-background/80 backdrop-blur-sm\"\n              onClick={handleEditSignature}\n              disabled={disabled}\n            >\n              <Pencil className=\"w-3 h-3 text-muted-foreground hover:text-primary\" />\n            </Button>\n            <Button\n              type=\"button\"\n              size=\"icon\"\n              variant=\"outline\"\n              className=\"h-7 w-7 rounded-full bg-background/80 backdrop-blur-sm\"\n              onClick={handleClearValue}\n              disabled={disabled}\n            >\n              <Eraser className=\"w-3 h-3 text-muted-foreground hover:text-primary\" />\n            </Button>\n          </div>\n        </div>\n\n        <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n          <DialogContent className=\"sm:max-w-md\">\n            <DialogHeader>\n              <DialogTitle className=\"flex items-center gap-2\">\n                <Pen className=\"w-4 h-4\" />\n                Draw Signature\n              </DialogTitle>\n              <DialogDescription>\n                Draw your signature below, then hold the confirm button to sign.\n              </DialogDescription>\n            </DialogHeader>\n\n            <div className=\"border border-input rounded-md overflow-hidden relative bg-background\">\n              <canvas\n                ref={canvasRef}\n                width={CANVAS_WIDTH}\n                height={CANVAS_HEIGHT}\n                className=\"w-full h-[200px] cursor-crosshair touch-none\"\n                onMouseDown={startDrawing}\n                onMouseUp={stopDrawing}\n                onMouseOut={stopDrawing}\n                onMouseMove={draw}\n                onTouchStart={startDrawing}\n                onTouchEnd={stopDrawing}\n                onTouchMove={draw}\n              />\n              <Button\n                type=\"button\"\n                size=\"icon\"\n                variant=\"outline\"\n                className=\"absolute left-2 bottom-2 rounded-full h-8 w-8\"\n                onClick={clearSignature}\n              >\n                <Eraser className=\"w-4 h-4 text-muted-foreground\" />\n              </Button>\n            </div>\n\n            <DialogFooter className=\"flex-row gap-2 sm:flex-row\">\n              <Button\n                type=\"button\"\n                variant=\"destructive\"\n                className=\"flex-shrink-0\"\n                onClick={handleReset}\n              >\n                <RotateCcw className=\"w-4 h-4 mr-2\" />\n                Reset\n              </Button>\n              <Button\n                type=\"button\"\n                className={cn(\n                  'flex-1 relative overflow-hidden transition-all border-2 border-transparent',\n                  hasDrawn\n                    ? 'bg-zinc-800 dark:bg-zinc-200 text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300'\n                    : 'bg-muted text-muted-foreground cursor-not-allowed',\n                )}\n                disabled={!hasDrawn}\n                onPointerDown={handleHoldStart}\n                onPointerUp={handleHoldEnd}\n                onPointerLeave={handleHoldEnd}\n                onPointerCancel={handleHoldEnd}\n              >\n                {/* Progress fill background */}\n                <span\n                  className=\"absolute left-0 top-0 bottom-0 bg-emerald-500 transition-none\"\n                  style={{ width: `${holdProgress}%` }}\n                />\n                <span className=\"relative z-10 flex items-center gap-2\">\n                  <Check className={cn('w-4 h-4', isHolding && 'animate-pulse')} />\n                  {isHolding ? 'Keep holding...' : 'Hold to confirm'}\n                </span>\n              </Button>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n      </div>\n    )\n  }\n\n  // Initial state: show pen button\n  return (\n    <div className={cn('inline-flex mt-2', className)}>\n      <Button\n        type=\"button\"\n        size=\"icon\"\n        variant=\"outline\"\n        className=\"rounded-full\"\n        onClick={() => setIsDialogOpen(true)}\n        disabled={disabled}\n      >\n        <Pen className=\"w-4 h-4\" />\n      </Button>\n\n      <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n        <DialogContent className=\"sm:max-w-md\">\n          <DialogHeader>\n            <DialogTitle className=\"flex items-center gap-2\">\n              <Pen className=\"w-4 h-4\" />\n              Draw Signature\n            </DialogTitle>\n            <DialogDescription>\n              Draw your signature below, then hold the confirm button to sign.\n            </DialogDescription>\n          </DialogHeader>\n\n          <div className=\"border border-input rounded-md overflow-hidden relative bg-background\">\n            <canvas\n              ref={canvasRef}\n              width={CANVAS_WIDTH}\n              height={CANVAS_HEIGHT}\n              className=\"w-full h-[200px] cursor-crosshair touch-none\"\n              onMouseDown={startDrawing}\n              onMouseUp={stopDrawing}\n              onMouseOut={stopDrawing}\n              onMouseMove={draw}\n              onTouchStart={startDrawing}\n              onTouchEnd={stopDrawing}\n              onTouchMove={draw}\n            />\n            <Button\n              type=\"button\"\n              size=\"icon\"\n              variant=\"outline\"\n              className=\"absolute left-2 bottom-2 rounded-full h-8 w-8\"\n              onClick={clearSignature}\n            >\n              <Eraser className=\"w-4 h-4 text-muted-foreground\" />\n            </Button>\n          </div>\n\n          <DialogFooter className=\"flex-row gap-2 sm:flex-row\">\n            <Button\n              type=\"button\"\n              className={cn(\n                'flex-1 relative overflow-hidden transition-all border-2 border-transparent',\n                hasDrawn\n                  ? 'bg-zinc-800 dark:bg-zinc-200 text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300'\n                  : 'bg-muted text-muted-foreground cursor-not-allowed',\n              )}\n              disabled={!hasDrawn}\n              onPointerDown={handleHoldStart}\n              onPointerUp={handleHoldEnd}\n              onPointerLeave={handleHoldEnd}\n              onPointerCancel={handleHoldEnd}\n            >\n              {/* Progress fill background */}\n              <span\n                className=\"absolute left-0 top-0 bottom-0 bg-emerald-500 transition-none\"\n                style={{ width: `${holdProgress}%` }}\n              />\n              <span className=\"relative z-10 flex items-center gap-2\">\n                <Check className={cn('w-4 h-4', isHolding && 'animate-pulse')} />\n                {isHolding ? 'Keep holding...' : 'Hold to confirm'}\n              </span>\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "input"
  ],
  "type": "registry:ui"
}