{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hold-button",
  "title": "Hold Button",
  "description": "Press and hold to confirm destructive actions. A fill sweeps across while held.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/hold-button/hold-button.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n  type AnimationPlaybackControlsWithThen,\n} from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface HoldButtonProps\n  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\" | \"children\"> {\n  /** Seconds the button must be held to confirm. Default: 1.2 */\n  holdDuration?: number\n  /** Idle label. Default: \"Hold to delete\" */\n  label?: string\n  /** Label shown once confirmed. Default: \"Deleted\" */\n  confirmedLabel?: string\n  /** Color of the fill that sweeps across while holding. Default: \"#ef4444\" */\n  fillColor?: string\n  /** Return to idle this many ms after confirming (0 = stay confirmed). Default: 1800 */\n  resetAfter?: number\n  /** Called once the hold completes. */\n  onConfirm?: () => void\n}\n\nconst Check = () => (\n  <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <motion.path\n      d=\"M3 8.5 6.5 12 13 4.5\"\n      initial={{ pathLength: 0 }}\n      animate={{ pathLength: 1 }}\n      transition={{ duration: 0.35, ease: \"easeOut\", delay: 0.05 }}\n    />\n  </svg>\n)\n\nexport function HoldButton({\n  holdDuration = 1.2,\n  label = \"Hold to delete\",\n  confirmedLabel = \"Deleted\",\n  fillColor = \"#ef4444\",\n  resetAfter = 1800,\n  onConfirm,\n  className,\n  disabled,\n  onPointerDown,\n  onKeyDown,\n  onKeyUp,\n  onBlur,\n  ...props\n}: HoldButtonProps) {\n  const reduced = useReducedMotion()\n  const [confirmed, setConfirmed] = useState(false)\n  const progress = useMotionValue(0)\n  const clipPath = useTransform(progress, (v) => `inset(0 ${(1 - v) * 100}% 0 0)`)\n  const controls = useRef<AnimationPlaybackControlsWithThen>(undefined)\n  const resetTimer = useRef<ReturnType<typeof setTimeout>>(undefined)\n\n  useEffect(\n    () => () => {\n      controls.current?.stop()\n      clearTimeout(resetTimer.current)\n    },\n    []\n  )\n\n  const complete = () => {\n    setConfirmed(true)\n    onConfirm?.()\n    if (resetAfter > 0) {\n      resetTimer.current = setTimeout(() => {\n        setConfirmed(false)\n        controls.current = animate(progress, 0, { duration: reduced ? 0 : 0.4, ease: [0.4, 0, 0.2, 1] })\n      }, resetAfter)\n    }\n  }\n\n  const start = () => {\n    if (confirmed || disabled) return\n    controls.current?.stop()\n    const remaining = holdDuration * (1 - progress.get())\n    const hold = animate(progress, 1, { duration: remaining, ease: \"linear\" })\n    controls.current = hold\n    hold.then(() => {\n      if (controls.current === hold && progress.get() >= 1) complete()\n    })\n  }\n\n  const cancel = () => {\n    if (confirmed || progress.get() >= 1) return\n    controls.current?.stop()\n    controls.current = reduced\n      ? animate(progress, 0, { duration: 0 })\n      : animate(progress, 0, { type: \"spring\", stiffness: 260, damping: 30 })\n  }\n\n  return (\n    <button\n      type=\"button\"\n      {...props}\n      disabled={disabled}\n      aria-live=\"polite\"\n      onPointerDown={(e) => {\n        onPointerDown?.(e)\n        if (e.button !== 0) return\n        e.currentTarget.setPointerCapture(e.pointerId)\n        start()\n      }}\n      onPointerUp={cancel}\n      onPointerCancel={cancel}\n      onLostPointerCapture={cancel}\n      onKeyDown={(e) => {\n        onKeyDown?.(e)\n        if (e.key !== \" \" && e.key !== \"Enter\") return\n        e.preventDefault()\n        if (!e.repeat) start()\n      }}\n      onKeyUp={(e) => {\n        onKeyUp?.(e)\n        if (e.key === \" \" || e.key === \"Enter\") cancel()\n      }}\n      onBlur={(e) => {\n        onBlur?.(e)\n        cancel()\n      }}\n      onContextMenu={(e) => e.preventDefault()}\n      className={cn(\n        \"relative inline-flex h-11 min-w-48 touch-none select-none items-center justify-center overflow-hidden rounded-full border bg-background px-6 text-sm font-medium text-foreground outline-none transition-transform active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n    >\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 origin-left\"\n        style={{ scaleX: progress, background: fillColor }}\n      />\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        <motion.span\n          key={confirmed ? \"done\" : \"idle\"}\n          className=\"relative flex items-center gap-2\"\n          initial={reduced ? { opacity: 0 } : { opacity: 0, y: 12, filter: \"blur(4px)\" }}\n          animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n          exit={reduced ? { opacity: 0 } : { opacity: 0, y: -12, filter: \"blur(4px)\" }}\n          transition={{ type: \"spring\", stiffness: 500, damping: 34 }}\n        >\n          {confirmed ? (\n            <span className=\"flex items-center gap-2 text-white\">\n              <Check />\n              {confirmedLabel}\n            </span>\n          ) : (\n            label\n          )}\n        </motion.span>\n      </AnimatePresence>\n      {/* white copy of the label, revealed by the fill so it stays legible */}\n      {!confirmed && (\n        <motion.span\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 flex items-center justify-center text-white\"\n          style={{ clipPath }}\n        >\n          {label}\n        </motion.span>\n      )}\n    </button>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/hold-button",
  "type": "registry:component"
}