{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "long-press-button",
  "type": "registry:component",
  "title": "Long Press Button",
  "description": "A long-press action button with animated hold progress.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/new-york/blocks/long-press-button/long-press-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_HOLD_DURATION = 1600;\ntype ProgressDirection = \"left-to-right\" | \"top-to-bottom\" | \"bottom-to-top\";\n\nexport type LongPressButtonProps = React.ComponentPropsWithoutRef<\"button\"> & {\n  holdDuration?: number;\n  onHoldComplete?: () => void;\n  onHoldCancel?: () => void;\n  progressClassName?: string;\n  progressDirection?: ProgressDirection;\n};\n\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\n\nconst getClipPath = (progress: number, direction: ProgressDirection) => {\n  const remaining = 100 - clamp(progress, 0, 1) * 100;\n  if (direction === \"top-to-bottom\") {\n    return `inset(0 0 ${remaining}% 0)`;\n  }\n  if (direction === \"bottom-to-top\") {\n    return `inset(${remaining}% 0 0 0)`;\n  }\n  return `inset(0 ${remaining}% 0 0)`;\n};\n\nconst LongPressButton = React.forwardRef<HTMLButtonElement, LongPressButtonProps>(\n  (\n    {\n      holdDuration = DEFAULT_HOLD_DURATION,\n      onHoldComplete,\n      onHoldCancel,\n      className,\n      progressClassName,\n      progressDirection = \"left-to-right\",\n      disabled,\n      type,\n      children,\n      onPointerDown,\n      onPointerUp,\n      onPointerLeave,\n      onPointerCancel,\n      onBlur,\n      onKeyDown,\n      onKeyUp,\n      ...props\n    },\n    ref\n  ) => {\n    const contentRef = React.useRef<HTMLSpanElement | null>(null);\n    const [progress, setProgress] = React.useState(0);\n    const [isHolding, setIsHolding] = React.useState(false);\n\n    const progressRef = React.useRef(0);\n    const holdRef = React.useRef<{\n      frame: number | null;\n      startTime: number | null;\n      completed: boolean;\n    }>({ frame: null, startTime: null, completed: false });\n\n    const setProgressValue = React.useCallback((value: number) => {\n      progressRef.current = value;\n      setProgress(value);\n    }, []);\n\n    const stopAnimation = React.useCallback(() => {\n      if (holdRef.current.frame !== null) {\n        cancelAnimationFrame(holdRef.current.frame);\n        holdRef.current.frame = null;\n      }\n    }, []);\n\n    const playCompleteAnimation = React.useCallback(() => {\n      if (typeof window !== \"undefined\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n        return;\n      }\n      const iconNode = contentRef.current?.querySelector(\"svg\");\n      const target = iconNode ?? contentRef.current;\n      target?.animate(\n        [\n          { transform: \"translateX(0) rotate(0deg)\" },\n          { transform: \"translateX(-1px) rotate(-6deg)\" },\n          { transform: \"translateX(1px) rotate(6deg)\" },\n          { transform: \"translateX(0) rotate(0deg)\" },\n        ],\n        { duration: 220, easing: \"ease-out\" }\n      );\n    }, []);\n\n    const resetHold = React.useCallback(() => {\n      holdRef.current.startTime = null;\n      holdRef.current.completed = false;\n      setIsHolding(false);\n      setProgressValue(0);\n    }, [setProgressValue]);\n\n    const stopHold = React.useCallback(() => {\n      const wasHolding = holdRef.current.frame !== null || isHolding;\n      stopAnimation();\n      const shouldNotify = wasHolding && !holdRef.current.completed;\n      resetHold();\n      if (shouldNotify) {\n        onHoldCancel?.();\n      }\n    }, [isHolding, onHoldCancel, resetHold, stopAnimation]);\n\n    const startHold = React.useCallback(() => {\n      if (disabled || holdRef.current.frame !== null || holdRef.current.completed) {\n        return;\n      }\n\n      const resolvedDuration = Math.max(holdDuration, 0);\n      if (resolvedDuration === 0) {\n        setProgressValue(1);\n        holdRef.current.completed = true;\n        setIsHolding(false);\n        playCompleteAnimation();\n        onHoldComplete?.();\n        return;\n      }\n\n      setIsHolding(true);\n      holdRef.current.startTime = null;\n      holdRef.current.frame = requestAnimationFrame(function tick(time) {\n        if (holdRef.current.startTime === null) {\n          holdRef.current.startTime = time;\n        }\n        const elapsed = time - holdRef.current.startTime;\n        const nextProgress = clamp(elapsed / resolvedDuration, 0, 1);\n        setProgressValue(nextProgress);\n\n        if (nextProgress >= 1) {\n          stopAnimation();\n          holdRef.current.completed = true;\n          setIsHolding(false);\n          playCompleteAnimation();\n          onHoldComplete?.();\n          return;\n        }\n\n        holdRef.current.frame = requestAnimationFrame(tick);\n      });\n    }, [disabled, holdDuration, onHoldComplete, playCompleteAnimation, setProgressValue, stopAnimation]);\n\n    React.useEffect(() => {\n      if (disabled) {\n        stopHold();\n      }\n    }, [disabled, stopHold]);\n\n    React.useEffect(() => {\n      return () => {\n        stopAnimation();\n      };\n    }, [stopAnimation]);\n\n    const state = progress >= 1 ? \"complete\" : isHolding ? \"holding\" : \"idle\";\n\n    return (\n      <button\n        ref={ref}\n        type={type ?? \"button\"}\n        data-slot=\"long-press-button\"\n        data-state={state}\n        disabled={disabled}\n        className={cn(\n          \"relative inline-flex items-center justify-center gap-2 overflow-hidden rounded-xl border border-destructive/40 bg-destructive/10 px-4 py-2 text-sm font-medium text-destructive shadow-sm transition-all duration-150 ease-out hover:bg-destructive/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40 disabled:pointer-events-none disabled:opacity-50\",\n          className\n        )}\n        onPointerDown={(event) => {\n          onPointerDown?.(event);\n          if (event.defaultPrevented || event.button !== 0) {\n            return;\n          }\n          startHold();\n        }}\n        onPointerUp={(event) => {\n          onPointerUp?.(event);\n          if (event.defaultPrevented) {\n            return;\n          }\n          stopHold();\n        }}\n        onPointerLeave={(event) => {\n          onPointerLeave?.(event);\n          if (event.defaultPrevented) {\n            return;\n          }\n          stopHold();\n        }}\n        onPointerCancel={(event) => {\n          onPointerCancel?.(event);\n          if (event.defaultPrevented) {\n            return;\n          }\n          stopHold();\n        }}\n        onBlur={(event) => {\n          onBlur?.(event);\n          if (event.defaultPrevented) {\n            return;\n          }\n          stopHold();\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (event.defaultPrevented || event.repeat) {\n            return;\n          }\n          if (event.key === \" \" || event.key === \"Enter\") {\n            event.preventDefault();\n            startHold();\n          }\n        }}\n        onKeyUp={(event) => {\n          onKeyUp?.(event);\n          if (event.defaultPrevented) {\n            return;\n          }\n          if (event.key === \" \" || event.key === \"Enter\") {\n            event.preventDefault();\n            stopHold();\n          }\n        }}\n        {...props}\n      >\n        <span\n          aria-hidden=\"true\"\n          className={cn(\"absolute inset-0 bg-destructive/60\", progressClassName)}\n          style={{ clipPath: getClipPath(progress, progressDirection) }}\n        />\n        <span ref={contentRef} className=\"relative z-10 flex items-center gap-2\">\n          {children ?? \"Long press button\"}\n        </span>\n      </button>\n    );\n  }\n);\n\nLongPressButton.displayName = \"LongPressButton\";\n\nexport { LongPressButton };\n",
      "type": "registry:component",
      "target": "components/ui/long-press-button.tsx"
    }
  ]
}