{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "primitive-hold-to-confirm",
  "type": "registry:ui",
  "title": "Hold to confirm",
  "description": "Show cancellable progress during hold-to-confirm interactions to reduce mistakes.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "primitive",
    "feedback"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/primitives/hold-to-confirm/",
  "files": [
    {
      "path": "src/registry/primitives/hold-to-confirm.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/primitives/hold-to-confirm.tsx",
      "content": "\"use client\";\n\nimport { animate, motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\n\nexport type HoldToConfirmPrimitiveProps = {\n  children: ReactNode;\n  confirmed: ReactNode;\n  onConfirm: () => void;\n  duration?: number;\n  holdScale?: number;\n  disabled?: boolean;\n  className?: string;\n};\n\nexport function HoldToConfirmPrimitive({\n  children,\n  confirmed,\n  onConfirm,\n  duration = 1.2,\n  holdScale = 0.98,\n  disabled = false,\n  className,\n}: HoldToConfirmPrimitiveProps) {\n  const reduceMotion = useReducedMotion();\n  const progress = useMotionValue(0);\n  const scaleX = useTransform(progress, [0, 1], [0, 1]);\n  const [holding, setHolding] = useState(false);\n  const [complete, setComplete] = useState(false);\n  const control = useRef<ReturnType<typeof animate> | null>(null);\n  const origin = useRef<{ x: number; y: number } | null>(null);\n\n  const cancel = () => {\n    if (progress.get() >= 1) return;\n    control.current?.stop();\n    setHolding(false);\n    origin.current = null;\n    const current = progress.get();\n    control.current = animate(progress, 0, {\n      duration: reduceMotion ? 0.08 : Math.max(0.08, current * 0.22),\n      ease: [0.23, 1, 0.32, 1],\n    });\n  };\n\n  const begin = (point?: { x: number; y: number }) => {\n    if (disabled || holding || complete) return;\n    setHolding(true);\n    origin.current = point ?? null;\n    const current = progress.get();\n    control.current?.stop();\n    control.current = animate(progress, 1, {\n      duration: duration * (1 - current),\n      ease: \"linear\",\n      onComplete: () => {\n        setHolding(false);\n        setComplete(true);\n        origin.current = null;\n        navigator.vibrate?.(12);\n        onConfirm();\n      },\n    });\n  };\n\n  useEffect(() => () => control.current?.stop(), []);\n\n  return (\n    <motion.button\n      type=\"button\"\n      className={`relative isolate overflow-hidden ${className ?? \"\"}`}\n      aria-disabled={disabled || complete}\n      animate={{ transform: holding && !reduceMotion ? `scale(${holdScale})` : \"scale(1)\" }}\n      transition={{ type: \"spring\", stiffness: 320, damping: 28 }}\n      onPointerDown={(event) => {\n        if (event.pointerType === \"mouse\" && event.button !== 0) return;\n        event.currentTarget.setPointerCapture(event.pointerId);\n        begin({ x: event.clientX, y: event.clientY });\n      }}\n      onPointerMove={(event) => {\n        const start = origin.current;\n        if (holding && start && Math.hypot(event.clientX - start.x, event.clientY - start.y) > 12) cancel();\n      }}\n      onPointerUp={cancel}\n      onPointerCancel={cancel}\n      onPointerLeave={cancel}\n      onKeyDown={(event) => {\n        if (!event.repeat && (event.key === \" \" || event.key === \"Enter\")) {\n          event.preventDefault();\n          begin();\n        }\n        if (event.key === \"Escape\") cancel();\n      }}\n      onKeyUp={(event) => {\n        if (event.key === \" \" || event.key === \"Enter\") cancel();\n      }}\n    >\n      {reduceMotion ? null : (\n        <motion.span aria-hidden className=\"absolute inset-0 origin-left bg-current opacity-15\" style={{ scaleX }} />\n      )}\n      <span className=\"relative\">{complete ? confirmed : children}</span>\n    </motion.button>\n  );\n}\n"
    }
  ]
}
