{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "HoldButton-TS-CSS",
	"title": "HoldButton",
	"description": "Hold-to-confirm button whose liquid fill rises while pressed, snaps back on an early release and swaps its label through a blur when the hold completes.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "HoldButton.css",
			"target": "@components/HoldButton.css",
			"content": ".hold-button {\n  --hb-radius: 14px;\n  --hb-bg: #27272a;\n  --hb-fill: #5227ff;\n  --hb-text: #f5f5f5;\n  --hb-fill-text: #ffffff;\n  --hb-hold: 2000ms;\n  --hb-release: 200ms;\n  --hb-press: 0.97;\n  --hb-wave: 6px;\n  --hb-w: 0px;\n  --hb-h: 0px;\n  --hb-cycles: 2;\n  --hb-p: 0;\n  --hb-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n  --hb-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);\n  --hb-glow:\n    inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 10px 32px -6px color-mix(in srgb, var(--hb-fill) 70%, transparent);\n\n  position: relative;\n  display: inline-grid;\n  place-items: center;\n  isolation: isolate;\n  margin: 0;\n  border: 0;\n  border-radius: var(--hb-radius);\n  background: var(--hb-bg);\n  color: var(--hb-text);\n  font-family: inherit;\n  font-weight: 500;\n  letter-spacing: 0.01em;\n  line-height: 1;\n  box-shadow: var(--hb-shadow);\n  cursor: pointer;\n  outline: none;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-touch-callout: none;\n  -webkit-tap-highlight-color: transparent;\n  touch-action: manipulation;\n  transition:\n    transform 160ms var(--hb-ease-out),\n    background-color 160ms ease,\n    box-shadow var(--hb-release) var(--hb-ease-out);\n}\n\n.hold-button--sm {\n  height: 36px;\n  padding: 0 16px;\n  font-size: 13px;\n}\n\n.hold-button--md {\n  height: 44px;\n  padding: 0 22px;\n  font-size: 15px;\n}\n\n.hold-button--lg {\n  height: 52px;\n  padding: 0 28px;\n  font-size: 17px;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .hold-button:not(:disabled):hover {\n    background: color-mix(in srgb, var(--hb-bg) 92%, #fff);\n  }\n}\n\n.hold-button[data-phase='holding'][data-input='pointer'] {\n  transform: scale(var(--hb-press));\n}\n\n.hold-button[data-glow='true'][data-phase='holding'],\n.hold-button[data-glow='true'][data-phase='done'] {\n  box-shadow: var(--hb-glow);\n}\n\n.hold-button[data-glow='true'][data-phase='holding'] {\n  transition:\n    transform 160ms var(--hb-ease-out),\n    background-color 160ms ease,\n    box-shadow var(--hb-hold) linear;\n}\n\n.hold-button:focus-visible {\n  outline: 2px solid var(--hb-fill);\n  outline-offset: 3px;\n}\n\n.hold-button:disabled {\n  opacity: 0.5;\n  cursor: default;\n  pointer-events: none;\n}\n\n.hold-button__pulse {\n  position: absolute;\n  inset: 0;\n  z-index: 0;\n  border-radius: var(--hb-radius);\n  pointer-events: none;\n  opacity: 0;\n}\n\n.hold-button[data-glow='true'][data-phase='done'] .hold-button__pulse {\n  animation: hb-pulse 600ms var(--hb-ease-out) forwards;\n}\n\n@keyframes hb-pulse {\n  from {\n    opacity: 1;\n    box-shadow: 0 0 0 0 color-mix(in srgb, var(--hb-fill) 55%, transparent);\n  }\n  to {\n    opacity: 0;\n    box-shadow: 0 0 0 14px color-mix(in srgb, var(--hb-fill) 0%, transparent);\n  }\n}\n\n.hold-button__label {\n  position: relative;\n  z-index: 2;\n  display: grid;\n  place-items: center;\n}\n\n.hold-button__label > span {\n  grid-area: 1 / 1;\n  display: inline-flex;\n  align-items: center;\n  gap: 8px;\n  white-space: nowrap;\n  transition:\n    opacity 200ms ease,\n    filter 200ms ease;\n}\n\n.hold-button__icon {\n  display: inline-flex;\n  flex: none;\n}\n\n.hold-button__icon > svg {\n  display: block;\n}\n\n.hold-button__done {\n  opacity: 0;\n  filter: blur(2px);\n}\n\n.hold-button[data-phase='done'] .hold-button__idle {\n  opacity: 0;\n  filter: blur(2px);\n}\n\n.hold-button[data-phase='done'] .hold-button__done {\n  opacity: 1;\n  filter: blur(0);\n}\n\n.hold-button__clip {\n  position: absolute;\n  inset: 0;\n  z-index: 3;\n  clip-path: inset(0 round var(--hb-radius));\n  pointer-events: none;\n}\n\n.hold-button__fill {\n  position: absolute;\n  inset: 0;\n  display: grid;\n  place-items: center;\n  background: var(--hb-fill);\n  color: var(--hb-fill-text);\n  clip-path: inset(\n    0 calc((1 - var(--hb-p)) * (100% + 0.75 * var(--hb-wave)) - var(--hb-p) * 0.25 * var(--hb-wave)) 0 0\n  );\n}\n\n.hold-button[data-direction='up'] .hold-button__fill {\n  clip-path: inset(\n    calc((1 - var(--hb-p)) * (100% + 0.75 * var(--hb-wave)) - var(--hb-p) * 0.25 * var(--hb-wave)) 0 0 0\n  );\n}\n\n.hold-button__crest {\n  position: absolute;\n  inset: 0;\n  display: grid;\n  place-items: center;\n  background: var(--hb-fill);\n  color: var(--hb-fill-text);\n  -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='200' viewBox='0 0 20 200' preserveAspectRatio='none'%3E%3Cpath d='M0 0H10C18 8 18 25.3 10 33.3S2 58.7 10 66.7S18 92 10 100S2 125.3 10 133.3S18 158.7 10 166.7S2 192 10 200H0Z'/%3E%3C/svg%3E\");\n  mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='200' viewBox='0 0 20 200' preserveAspectRatio='none'%3E%3Cpath d='M0 0H10C18 8 18 25.3 10 33.3S2 58.7 10 66.7S18 92 10 100S2 125.3 10 133.3S18 158.7 10 166.7S2 192 10 200H0Z'/%3E%3C/svg%3E\");\n  -webkit-mask-repeat: repeat-y;\n  mask-repeat: repeat-y;\n  -webkit-mask-size: var(--hb-wave) calc(var(--hb-h) * 2);\n  mask-size: var(--hb-wave) calc(var(--hb-h) * 2);\n  -webkit-mask-position-x: calc(-1 * var(--hb-wave) + var(--hb-p) * (var(--hb-w) + var(--hb-wave)));\n  mask-position-x: calc(-1 * var(--hb-wave) + var(--hb-p) * (var(--hb-w) + var(--hb-wave)));\n  -webkit-mask-position-y: calc(-1 * var(--hb-p) * var(--hb-cycles) * var(--hb-h));\n  mask-position-y: calc(-1 * var(--hb-p) * var(--hb-cycles) * var(--hb-h));\n}\n\n.hold-button[data-direction='up'] .hold-button__crest {\n  -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='20' viewBox='0 0 200 20' preserveAspectRatio='none'%3E%3Cpath d='M0 20V10C12 2 38 2 50 10S88 18 100 10S138 2 150 10S188 18 200 10V20Z'/%3E%3C/svg%3E\");\n  mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='20' viewBox='0 0 200 20' preserveAspectRatio='none'%3E%3Cpath d='M0 20V10C12 2 38 2 50 10S88 18 100 10S138 2 150 10S188 18 200 10V20Z'/%3E%3C/svg%3E\");\n  -webkit-mask-repeat: repeat-x;\n  mask-repeat: repeat-x;\n  -webkit-mask-size: calc(var(--hb-w) * 2) var(--hb-wave);\n  mask-size: calc(var(--hb-w) * 2) var(--hb-wave);\n  -webkit-mask-position-x: calc(-1 * var(--hb-p) * var(--hb-cycles) * var(--hb-w));\n  mask-position-x: calc(-1 * var(--hb-p) * var(--hb-cycles) * var(--hb-w));\n  -webkit-mask-position-y: calc(var(--hb-h) - var(--hb-p) * (var(--hb-h) + var(--hb-wave)));\n  mask-position-y: calc(var(--hb-h) - var(--hb-p) * (var(--hb-h) + var(--hb-wave)));\n}\n\n.hold-button__label--fill {\n  z-index: auto;\n}\n\n.hold-button__sr {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  overflow: hidden;\n  clip-path: inset(50%);\n  white-space: nowrap;\n}\n\n@media (prefers-contrast: more) {\n  .hold-button {\n    outline: 1px solid var(--hb-text);\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .hold-button {\n    transition:\n      background-color 160ms ease,\n      box-shadow var(--hb-release) ease;\n    transform: none !important;\n  }\n\n  .hold-button__fill,\n  .hold-button[data-direction='up'] .hold-button__fill {\n    clip-path: inset(0);\n    opacity: 0;\n    transition: opacity var(--hb-release) ease;\n  }\n\n  .hold-button__crest {\n    display: none;\n  }\n\n  .hold-button[data-phase='holding'] .hold-button__fill,\n  .hold-button[data-phase='done'] .hold-button__fill {\n    opacity: 1;\n    transition: opacity var(--hb-hold) linear;\n  }\n\n  .hold-button__pulse {\n    animation: none !important;\n  }\n\n  .hold-button__label > span {\n    transition: opacity 200ms ease;\n    filter: none !important;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "HoldButton.tsx",
			"content": "'use client';\n\nimport React, { useEffect, useId, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';\n\nimport './HoldButton.css';\n\nexport type HoldButtonSize = 'sm' | 'md' | 'lg';\nexport type HoldButtonDirection = 'right' | 'up';\n\nexport interface HoldButtonProps {\n  children?: ReactNode;\n  doneLabel?: ReactNode;\n  icon?: ReactNode;\n  doneIcon?: ReactNode;\n  backgroundColor?: string;\n  fillColor?: string;\n  textColor?: string;\n  fillTextColor?: string;\n  size?: HoldButtonSize;\n  radius?: number;\n  fillDirection?: HoldButtonDirection;\n  holdTime?: number;\n  releaseTime?: number;\n  pressScale?: number;\n  wave?: boolean;\n  waveAmplitude?: number;\n  glow?: boolean;\n  resetAfter?: number;\n  disabled?: boolean;\n  onHold?: () => void;\n  onTap?: () => void;\n  className?: string;\n}\n\ntype Phase = 'idle' | 'holding' | 'done';\ntype Input = 'pointer' | 'key' | null;\n\ninterface Motion {\n  raf: number;\n  p: number;\n  from: number;\n  to: number;\n  start: number;\n}\n\ninterface Gesture {\n  pointerId: number | null;\n  start: number;\n  rect: DOMRect | null;\n}\n\ninterface ReleaseOptions {\n  drifted?: boolean;\n}\n\nconst TAP_MS = 250;\nconst HIT_PAD = 10;\nconst LINEAR = (t: number) => t;\nconst EASE_OUT = (t: number) => 1 - Math.pow(1 - t, 3);\n\nconst HoldButton: React.FC<HoldButtonProps> = ({\n  children = 'Hold to delete',\n  doneLabel = 'Deleted',\n  icon = null,\n  doneIcon = null,\n  backgroundColor = '#27272a',\n  fillColor = '#5227FF',\n  textColor = '#f5f5f5',\n  fillTextColor = '#ffffff',\n  size = 'md',\n  radius = 14,\n  fillDirection = 'right',\n  holdTime = 2000,\n  releaseTime = 200,\n  pressScale = 0.97,\n  wave = true,\n  waveAmplitude = 6,\n  glow = true,\n  resetAfter = 1200,\n  disabled = false,\n  onHold,\n  onTap,\n  className = ''\n}) => {\n  const [phase, setPhase] = useState<Phase>('idle');\n  const [input, setInput] = useState<Input>(null);\n  const phaseRef = useRef<Phase>('idle');\n  const inputRef = useRef<Input>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const gesture = useRef<Gesture>({ pointerId: null, start: 0, rect: null });\n  const timers = useRef({ complete: 0, reset: 0 });\n  const hintId = useId();\n\n  const go = (next: Phase, kind: Input = null) => {\n    phaseRef.current = next;\n    inputRef.current = kind;\n    setPhase(next);\n    setInput(kind);\n  };\n\n  const clearTimers = () => {\n    clearTimeout(timers.current.complete);\n    clearTimeout(timers.current.reset);\n  };\n\n  const motion = useRef<Motion>({ raf: 0, p: 0, from: 0, to: 0, start: 0 });\n  const drive = (to: number, duration: number, ease: (t: number) => number) => {\n    const m = motion.current;\n    cancelAnimationFrame(m.raf);\n    m.from = m.p;\n    m.to = to;\n    m.start = performance.now();\n    const step = (now: number) => {\n      const t = duration > 0 ? Math.min(1, (now - m.start) / duration) : 1;\n      m.p = m.from + (m.to - m.from) * ease(t);\n      buttonRef.current?.style.setProperty('--hb-p', m.p.toFixed(4));\n      if (t < 1) {\n        m.raf = requestAnimationFrame(step);\n        return;\n      }\n      m.raf = 0;\n      if (m.to === 1) complete();\n    };\n    m.raf = requestAnimationFrame(step);\n  };\n\n  const complete = () => {\n    if (phaseRef.current !== 'holding') return;\n    if (performance.now() - gesture.current.start < holdTime - 50) return;\n    clearTimers();\n    go('done', inputRef.current);\n    onHold?.();\n    if (resetAfter > 0) {\n      timers.current.reset = window.setTimeout(() => {\n        go('idle');\n        drive(0, releaseTime, EASE_OUT);\n      }, resetAfter);\n    }\n  };\n\n  const begin = (kind: Input) => {\n    if (disabled || phaseRef.current !== 'idle') return false;\n    const button = buttonRef.current;\n    if (!button) return false;\n    gesture.current.start = performance.now();\n    gesture.current.rect = button.getBoundingClientRect();\n    go('holding', kind);\n    drive(1, holdTime, LINEAR);\n    timers.current.complete = window.setTimeout(complete, holdTime + 100);\n    return true;\n  };\n\n  const release = ({ drifted = false }: ReleaseOptions = {}) => {\n    if (phaseRef.current !== 'holding') return;\n    clearTimers();\n    const held = performance.now() - gesture.current.start;\n    go('idle');\n    drive(0, releaseTime, EASE_OUT);\n    if (!drifted && held < TAP_MS) onTap?.();\n  };\n  const releaseRef = useRef(release);\n  releaseRef.current = release;\n\n  const handlePointerDown = (e: React.PointerEvent<HTMLButtonElement>) => {\n    if (e.button !== 0 || !e.isPrimary || gesture.current.pointerId !== null) return;\n    if (!begin('pointer')) return;\n    gesture.current.pointerId = e.pointerId;\n    try {\n      e.currentTarget.setPointerCapture(e.pointerId);\n    } catch {}\n  };\n\n  const endPointer = (e: React.PointerEvent<HTMLButtonElement>, options?: ReleaseOptions) => {\n    if (e.pointerId !== gesture.current.pointerId) return;\n    gesture.current.pointerId = null;\n    try {\n      if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {}\n    release(options);\n  };\n\n  const handlePointerMove = (e: React.PointerEvent<HTMLButtonElement>) => {\n    if (e.pointerId !== gesture.current.pointerId) return;\n    const r = gesture.current.rect;\n    if (!r) return;\n    const out =\n      e.clientX < r.left - HIT_PAD ||\n      e.clientX > r.right + HIT_PAD ||\n      e.clientY < r.top - HIT_PAD ||\n      e.clientY > r.bottom + HIT_PAD;\n    if (out) endPointer(e, { drifted: true });\n  };\n\n  const handlePointerLeave = (e: React.PointerEvent<HTMLButtonElement>) => {\n    if (e.pointerType !== 'touch') endPointer(e, { drifted: true });\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n    if (e.key === 'Escape') {\n      if (inputRef.current === 'key') release({ drifted: true });\n      return;\n    }\n    if (e.key === ' ' || e.key === 'Enter') {\n      e.preventDefault();\n      if (!e.repeat) begin('key');\n    }\n  };\n\n  const handleKeyUp = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n    if (e.key === ' ' || e.key === 'Enter') {\n      e.preventDefault();\n      if (inputRef.current === 'key') release();\n    }\n  };\n\n  useLayoutEffect(() => {\n    const button = buttonRef.current;\n    if (!button) return undefined;\n    const measure = () => {\n      button.style.setProperty('--hb-w', `${button.offsetWidth}px`);\n      button.style.setProperty('--hb-h', `${button.offsetHeight}px`);\n    };\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(button);\n    return () => ro.disconnect();\n  }, []);\n\n  useEffect(() => {\n    if (phase !== 'holding') return undefined;\n    const cancel = () => releaseRef.current({ drifted: true });\n    const onVisibility = () => {\n      if (document.hidden) cancel();\n    };\n    window.addEventListener('blur', cancel);\n    document.addEventListener('visibilitychange', onVisibility);\n    return () => {\n      window.removeEventListener('blur', cancel);\n      document.removeEventListener('visibilitychange', onVisibility);\n    };\n  }, [phase]);\n\n  useEffect(() => {\n    const t = timers.current;\n    const m = motion.current;\n    return () => {\n      clearTimeout(t.complete);\n      clearTimeout(t.reset);\n      cancelAnimationFrame(m.raf);\n    };\n  }, []);\n\n  const direction: HoldButtonDirection = fillDirection === 'up' ? 'up' : 'right';\n  const labels = (\n    <>\n      <span className=\"hold-button__idle\" aria-hidden={phase === 'done'}>\n        {icon ? <span className=\"hold-button__icon\">{icon}</span> : null}\n        {children}\n      </span>\n      <span className=\"hold-button__done\" aria-hidden={phase !== 'done'}>\n        {doneIcon ? <span className=\"hold-button__icon\">{doneIcon}</span> : null}\n        {doneLabel}\n      </span>\n    </>\n  );\n\n  const cssVars = {\n    '--hb-radius': `${radius}px`,\n    '--hb-bg': backgroundColor,\n    '--hb-fill': fillColor,\n    '--hb-text': textColor,\n    '--hb-fill-text': fillTextColor,\n    '--hb-hold': `${holdTime}ms`,\n    '--hb-cycles': holdTime / 1100,\n    '--hb-release': `${releaseTime}ms`,\n    '--hb-press': pressScale,\n    '--hb-wave': `${wave ? waveAmplitude : 0}px`\n  } as CSSProperties;\n\n  return (\n    <button\n      ref={buttonRef}\n      type=\"button\"\n      disabled={disabled}\n      className={`hold-button hold-button--${size}${className ? ` ${className}` : ''}`}\n      data-phase={phase}\n      data-input={input ?? undefined}\n      data-direction={direction}\n      data-glow={glow ? 'true' : undefined}\n      aria-describedby={hintId}\n      style={cssVars}\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={e => endPointer(e)}\n      onPointerCancel={e => endPointer(e, { drifted: true })}\n      onLostPointerCapture={e => endPointer(e, { drifted: true })}\n      onPointerLeave={handlePointerLeave}\n      onKeyDown={handleKeyDown}\n      onKeyUp={handleKeyUp}\n      onContextMenu={e => e.preventDefault()}\n    >\n      <span className=\"hold-button__pulse\" aria-hidden=\"true\" />\n      <span className=\"hold-button__label\">{labels}</span>\n      <span className=\"hold-button__clip\" aria-hidden=\"true\">\n        <span className=\"hold-button__fill\">\n          <span className=\"hold-button__label hold-button__label--fill\">{labels}</span>\n        </span>\n        <span className=\"hold-button__crest\" aria-hidden=\"true\">\n          <span className=\"hold-button__label hold-button__label--fill\">{labels}</span>\n        </span>\n      </span>\n      <span id={hintId} className=\"hold-button__sr\">\n        Press and hold for {Math.round(holdTime / 100) / 10} seconds to confirm\n      </span>\n    </button>\n  );\n};\n\nexport default HoldButton;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}