{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hold-confirm-button",
  "title": "Hold Confirm Button",
  "description": "Press and hold to confirm a destructive action; a radial fill commits at 100% and releasing early cancels.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/hold-confirm-button/hold-confirm-button.tsx",
      "content": "\"use client\";\n\nimport { animate, motion, useMotionValue } from \"framer-motion\";\nimport * as React from \"react\";\n\nexport type HoldConfirmButtonVariant = \"destructive\" | \"default\";\nexport type HoldConfirmButtonSize = \"sm\" | \"md\" | \"lg\";\nexport type HoldConfirmButtonStatus = \"idle\" | \"holding\" | \"confirmed\";\n\nexport type HoldConfirmButtonProps = Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  \"onClick\"\n> & {\n  /** Fires once the hold completes. */\n  onConfirm?: () => void;\n  variant?: HoldConfirmButtonVariant;\n  size?: HoldConfirmButtonSize;\n  /** ms the user must hold to confirm. Default `900`. */\n  duration?: number;\n  /** Label shown while holding. Default `\"Hold to confirm\"`. */\n  holdingLabel?: React.ReactNode;\n  /** Label shown after confirming. Default `\"Confirmed\"`. */\n  confirmedLabel?: React.ReactNode;\n};\n\nconst BUTTON_BASE =\n  \"relative inline-flex cursor-pointer select-none items-center justify-center gap-2 overflow-hidden rounded-[var(--button-radius)] font-medium [outline-offset:4px] [-webkit-tap-highlight-color:transparent] focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background [transition:scale_120ms_ease] active:scale-[0.99] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50\";\n\nconst variantClass: Record<HoldConfirmButtonVariant, string> = {\n  destructive: \"bg-destructive text-white shadow-sm\",\n  default: \"bg-primary text-primary-foreground shadow-sm\",\n};\n\n// The fill that sweeps in from the left as the hold progresses.\nconst fillClass: Record<HoldConfirmButtonVariant, string> = {\n  destructive: \"bg-black/25\",\n  default: \"bg-black/20\",\n};\n\nconst sizeClass: Record<HoldConfirmButtonSize, string> = {\n  sm: \"[--button-radius:var(--button-radius-sm)] px-[var(--button-px-sm)] py-[var(--button-py-sm)] text-[length:var(--button-text-sm)] leading-[var(--button-leading-sm)]\",\n  md: \"[--button-radius:var(--button-radius-md)] px-[var(--button-px-md)] py-[var(--button-py-md)] text-[length:var(--button-text-md)] leading-[var(--button-leading-md)]\",\n  lg: \"[--button-radius:var(--button-radius-lg)] px-[var(--button-px-lg)] py-[var(--button-py-lg)] text-[length:var(--button-text-lg)] leading-[var(--button-leading-lg)]\",\n};\n\nconst HoldConfirmButton = React.forwardRef<\n  HTMLButtonElement,\n  HoldConfirmButtonProps\n>(\n  (\n    {\n      onConfirm,\n      variant = \"destructive\",\n      size = \"md\",\n      duration = 900,\n      holdingLabel = \"Confirming…\",\n      confirmedLabel = \"Confirmed\",\n      className,\n      children = \"Hold to confirm\",\n      disabled,\n      onKeyDown,\n      onKeyUp,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const [status, setStatus] = React.useState<HoldConfirmButtonStatus>(\"idle\");\n    const statusRef = React.useRef(status);\n    statusRef.current = status;\n\n    const progress = useMotionValue(0);\n    const playback = React.useRef<ReturnType<typeof animate> | null>(null);\n    const resetTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined);\n    const mounted = React.useRef(true);\n    React.useEffect(() => {\n      mounted.current = true;\n      return () => {\n        mounted.current = false;\n        playback.current?.stop();\n        clearTimeout(resetTimer.current);\n      };\n    }, []);\n\n    const complete = () => {\n      if (!mounted.current) return;\n      setStatus(\"confirmed\");\n      onConfirm?.();\n      resetTimer.current = setTimeout(() => {\n        if (!mounted.current) return;\n        setStatus(\"idle\");\n        animate(progress, 0, { duration: 0.2 });\n      }, 1100);\n    };\n\n    const start = () => {\n      if (statusRef.current !== \"idle\" || disabled) return;\n      setStatus(\"holding\");\n      playback.current = animate(progress, 1, {\n        duration: duration / 1000,\n        ease: \"linear\",\n        onComplete: complete,\n      });\n    };\n\n    const cancel = () => {\n      if (statusRef.current !== \"holding\") return;\n      playback.current?.stop();\n      setStatus(\"idle\");\n      animate(progress, 0, {\n        type: \"spring\",\n        stiffness: 320,\n        damping: 32,\n        mass: 0.9,\n      });\n    };\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      onKeyDown?.(event);\n      if ((event.key === \" \" || event.key === \"Enter\") && !event.repeat) {\n        event.preventDefault();\n        start();\n      }\n    };\n    const handleKeyUp = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      onKeyUp?.(event);\n      if (event.key === \" \" || event.key === \"Enter\") cancel();\n    };\n\n    // All label variants share one grid cell so the button sizes to the\n    // widest and never changes width as the status (and label) changes.\n    const labels: { key: HoldConfirmButtonStatus; node: React.ReactNode }[] = [\n      { key: \"idle\", node: children },\n      { key: \"holding\", node: holdingLabel },\n      { key: \"confirmed\", node: confirmedLabel },\n    ];\n\n    return (\n      <button\n        ref={forwardedRef}\n        type=\"button\"\n        data-status={status}\n        aria-label={typeof children === \"string\" ? children : undefined}\n        disabled={disabled}\n        onPointerDown={start}\n        onPointerUp={cancel}\n        onPointerLeave={cancel}\n        onPointerCancel={cancel}\n        onKeyDown={handleKeyDown}\n        onKeyUp={handleKeyUp}\n        className={`${BUTTON_BASE} ${variantClass[variant]} ${status === \"confirmed\" ? \"saturate-150\" : \"\"} ${sizeClass[size]} ${className ?? \"\"}`}\n        {...props}\n      >\n        <motion.span\n          aria-hidden=\"true\"\n          style={{ scaleX: progress }}\n          className={`absolute inset-0 origin-left ${fillClass[variant]}`}\n        />\n        <span className=\"relative grid\">\n          {labels.map(({ key, node }) => {\n            const active = status === key;\n            return (\n              <span\n                key={key}\n                aria-hidden={active ? undefined : \"true\"}\n                className={`col-start-1 row-start-1 inline-flex items-center justify-center gap-2 ${active ? \"\" : \"invisible\"}`}\n              >\n                {key === \"confirmed\" && (\n                  <svg\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    className=\"size-[1.15em]\"\n                    aria-hidden=\"true\"\n                  >\n                    <motion.path\n                      d=\"M5 12.5 10 17.5 19 7\"\n                      stroke=\"currentColor\"\n                      strokeWidth=\"2.5\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      initial={{ pathLength: 0 }}\n                      animate={active ? { pathLength: 1 } : { pathLength: 0 }}\n                      transition={{ duration: 0.3 }}\n                    />\n                  </svg>\n                )}\n                <span className=\"whitespace-nowrap\">{node}</span>\n              </span>\n            );\n          })}\n        </span>\n      </button>\n    );\n  },\n);\nHoldConfirmButton.displayName = \"HoldConfirmButton\";\n\nexport { HoldConfirmButton };\n",
      "type": "registry:ui",
      "target": "components/godui/hold-confirm-button.tsx"
    }
  ],
  "type": "registry:ui"
}
