{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SlideCommit-TS-TW",
	"title": "SlideCommit",
	"description": "Slide-to-confirm handle that spins while your action runs and unfurls into a done pill.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "SlideCommit/SlideCommit.tsx",
			"content": "'use client';\n\nimport React, { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';\nimport { animate, motion, useMotionValue, useMotionValueEvent, useReducedMotion, useTransform } from 'motion/react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { ArrowRight02Icon, Tick02Icon } from '@hugeicons/core-free-icons';\n\nexport type SlideCommitPhase = 'idle' | 'pending' | 'done' | 'error';\n\nexport interface SlideCommitProps {\n  label?: ReactNode;\n  doneLabel?: ReactNode;\n  errorLabel?: ReactNode;\n  onConfirm?: () => void | Promise<unknown>;\n  onDone?: () => void;\n  onError?: (reason: unknown) => void;\n  trackColor?: string;\n  handleColor?: string;\n  successColor?: string;\n  dangerColor?: string;\n  width?: number;\n  height?: number;\n  radius?: number;\n  speed?: number;\n  returnBounce?: number;\n  landingDip?: number;\n  holdMs?: number;\n  disabled?: boolean;\n  icon?: ReactNode;\n  className?: string;\n}\n\ntype Sample = [number, number];\ntype Grip = { id: number; grab: number | null; moved: boolean; hist: Sample[] };\ntype MoveEvent = { pointerId: number; clientX: number; timeStamp: number };\ntype UpEvent = { pointerId: number };\n\nconst PAD = 4;\nconst SQUASH_MAX = 0.08;\nconst SQUASH_DIV = 110;\nconst SWELL = 1.03;\nconst MIN_PENDING = 300;\nconst EASE_OUT: [number, number, number, number] = [0.23, 1, 0.32, 1];\nconst SHAKE = [0, -5, 5, -3, 3, -1, 0];\nconst STYLE = `\n@keyframes sc-spin { to { transform: rotate(360deg); } }\n.sc-spinner { animation: sc-spin 1s linear infinite; animation-play-state: paused; }\n.sc-root[data-phase='pending'] .sc-spinner { animation-play-state: running; }\n@media (prefers-reduced-motion: reduce) {\n  .sc-spinner { animation: sc-breathe 1.4s ease-in-out infinite; animation-play-state: paused; }\n  .sc-root[data-phase='pending'] .sc-spinner { animation-play-state: running; }\n  @keyframes sc-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }\n}\n`;\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\nconst onColor = (hex: string) => {\n  const raw = hex.replace('#', '');\n  const full = raw.length === 3 ? [...raw].map(ch => ch + ch).join('') : raw.slice(0, 6);\n  const n = parseInt(full, 16);\n  if (Number.isNaN(n)) return '#ffffff';\n  const yiq = (((n >> 16) & 255) * 299 + ((n >> 8) & 255) * 587 + (n & 255) * 114) / 1000;\n  return yiq >= 128 ? '#111111' : '#ffffff';\n};\nconst velocityOf = (hist: Sample[]) => {\n  if (hist.length < 2) return 0;\n  const [t0, x0] = hist[0];\n  const [t1, x1] = hist[hist.length - 1];\n  return ((x1 - x0) / Math.max(1, t1 - t0)) * 1000;\n};\nconst finePointer = () =>\n  typeof window !== 'undefined' && !!window.matchMedia?.('(hover: hover) and (pointer: fine)').matches;\n\nconst Spinner = ({ size }: { size: number }) => (\n  <svg className=\"sc-spinner block\" width={size} height={size} viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n    <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.4\" strokeOpacity=\"0.25\" />\n    <path d=\"M12 3a9 9 0 0 1 9 9\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.4\" strokeLinecap=\"round\" />\n  </svg>\n);\n\nconst SlideCommit: React.FC<SlideCommitProps> = ({\n  label = 'Slide to pay',\n  doneLabel = 'Paid',\n  errorLabel = 'Payment failed',\n  onConfirm,\n  onDone,\n  onError,\n  trackColor = '#262626',\n  handleColor = '#f5f5f5',\n  successColor = '#22c55e',\n  dangerColor = '#e5484d',\n  width = 280,\n  height = 56,\n  radius = 28,\n  speed = 50,\n  returnBounce = 0.38,\n  landingDip = 0.026,\n  holdMs = 1500,\n  disabled = false,\n  icon,\n  className = ''\n}) => {\n  const reduce = useReducedMotion();\n  const [phase, setPhase] = useState<SlideCommitPhase>('idle');\n  const [held, setHeld] = useState(false);\n  const [hot, setHot] = useState(false);\n\n  const trackRef = useRef<HTMLDivElement>(null);\n  const capsuleRef = useRef<HTMLDivElement>(null);\n  const grip = useRef<Grip | null>(null);\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const homeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const run = useRef(0);\n  const unwatch = useRef<(() => void) | null>(null);\n  const live = useRef<{ move: (e: MoveEvent) => void; up: (e: UpEvent) => void }>({ move: () => {}, up: () => {} });\n  const lastPercent = useRef(0);\n\n  const GRIP = height - PAD * 2;\n  const INNER = width - PAD * 2;\n  const TRAVEL = Math.max(1, INNER - GRIP);\n  const r = clamp(radius, 0, height / 2);\n  const gripR = Math.max(0, r - PAD);\n  const k = 260 + (clamp(speed, 0, 100) / 100) * 640;\n  const mass = 0.9;\n  const critical = 2 * Math.sqrt(k * mass);\n  const commitSpring = { type: 'spring' as const, stiffness: k, damping: critical, mass };\n  const homeSpring = { ...commitSpring, damping: critical * (1 - clamp(returnBounce, 0, 0.5)) };\n\n  const x = useMotionValue(0);\n  const anchor = useMotionValue(0);\n  const shown = useMotionValue(1);\n  const spin = useMotionValue(0);\n  const pulse = useMotionValue(1);\n  const shake = useMotionValue(0);\n  const seen = useTransform(x, v => clamp(v, 0, TRAVEL));\n  const edge = useTransform([seen, anchor], ([v, a]: number[]) => v + GRIP + clamp(a - v, 0, TRAVEL));\n  const clip = useTransform(edge, R => `inset(0 ${INNER - R}px 0 0 round ${gripR}px)`);\n  const content = useTransform([seen, edge], ([v, R]: number[]) => `translateX(${(v + R) / 2 - INNER / 2}px)`);\n  const swell = hot && !held && phase === 'idle' && !reduce ? SWELL : 1;\n  const shape = useTransform(x, v => {\n    const q = 1 - Math.min(SQUASH_MAX, Math.max(0, -v) / SQUASH_DIV);\n    return `scale(${q * swell}, ${swell / q})`;\n  });\n  const origin = useTransform(seen, v => `${v}px 50%`);\n  const say = useTransform(seen, [0, TRAVEL * 0.55], [1, 0]);\n  const arrow = useTransform(\n    [seen, shown],\n    ([v, on]: number[]) => on * clamp(1 - (v - TRAVEL * 0.55) / (TRAVEL * 0.4), 0, 1)\n  );\n  const trackTransform = useTransform([shake, pulse], ([s, p]: number[]) => `translateX(${s}px) scale(${p})`);\n\n  const labelText = typeof label === 'string' ? label : 'Slide to confirm';\n  useMotionValueEvent(seen, 'change', v => {\n    const percent = Math.round((v / TRAVEL) * 100);\n    if (percent === lastPercent.current || !capsuleRef.current) return;\n    lastPercent.current = percent;\n    capsuleRef.current.setAttribute('aria-valuenow', String(percent));\n    capsuleRef.current.setAttribute('aria-valuetext', `${labelText}, ${percent}%`);\n  });\n\n  useEffect(\n    () => () => {\n      clearTimeout(timer.current);\n      clearTimeout(homeTimer.current);\n      unwatch.current?.();\n      run.current += 1;\n    },\n    []\n  );\n\n  const local = (clientX: number) => {\n    const rect = trackRef.current?.getBoundingClientRect();\n    if (!rect) return 0;\n    return (clientX - rect.left) / (rect.width / width || 1);\n  };\n\n  const goHome = (velocity: number) => {\n    if (reduce) animate(x, 0, { duration: 0.2, ease: EASE_OUT });\n    else animate(x, 0, { ...homeSpring, velocity: Math.min(0, velocity) });\n  };\n\n  const settle = () => {\n    setPhase('idle');\n    animate(shown, 1, { duration: 0.2, delay: 0.12 });\n    if (reduce) anchor.set(0);\n    else animate(anchor, 0, { type: 'spring', duration: 0.3, bounce: 0 });\n  };\n\n  const resolve = (viaKey: boolean) => {\n    setPhase('done');\n    anchor.set(x.get());\n    animate(spin, 0, { duration: 0.12 });\n    if (reduce) x.set(0);\n    else {\n      animate(x, 0, commitSpring);\n      if (!viaKey && landingDip > 0) {\n        animate(pulse, [1, 1 - landingDip, 1], { duration: 0.46, times: [0, 0.62, 1], ease: EASE_OUT, delay: 0.1 });\n      }\n    }\n    onDone?.();\n    if (holdMs > 0) timer.current = setTimeout(settle, holdMs);\n  };\n\n  const reject = (reason: unknown) => {\n    setPhase('error');\n    onError?.(reason);\n    animate(spin, 0, { duration: 0.12 });\n    animate(shown, 1, { duration: 0.2, delay: 0.12 });\n    if (reduce) goHome(0);\n    else {\n      animate(shake, SHAKE, { duration: 0.45, ease: EASE_OUT });\n      homeTimer.current = setTimeout(() => {\n        if (!grip.current) goHome(0);\n      }, 300);\n    }\n    timer.current = setTimeout(() => setPhase('idle'), Math.max(holdMs, 1500));\n  };\n\n  const commit = (viaKey: boolean) => {\n    clearTimeout(timer.current);\n    const id = ++run.current;\n    x.set(TRAVEL);\n    let out: void | Promise<unknown>;\n    try {\n      out = onConfirm?.();\n    } catch (reason) {\n      reject(reason);\n      return;\n    }\n    const pending = out && typeof out.then === 'function' ? out : null;\n    if (!pending) {\n      animate(shown, 0, { duration: 0.12 });\n      resolve(viaKey);\n      return;\n    }\n    setPhase('pending');\n    animate(shown, 0, { duration: 0.2 });\n    animate(spin, 1, { duration: 0.2 });\n    const t0 = performance.now();\n    const later = (fn: () => void) => {\n      setTimeout(\n        () => {\n          if (id === run.current) fn();\n        },\n        Math.max(0, MIN_PENDING - (performance.now() - t0))\n      );\n    };\n    pending.then(\n      () => later(() => resolve(viaKey)),\n      reason => later(() => reject(reason))\n    );\n  };\n\n  const down = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (disabled || grip.current || phase === 'pending' || phase === 'done' || e.button !== 0) return;\n    x.stop();\n    grip.current = { id: e.pointerId, grab: null, moved: false, hist: [] };\n    setHeld(true);\n    try {\n      trackRef.current?.setPointerCapture(e.pointerId);\n    } catch {}\n    unwatch.current?.();\n    const onMove = (ev: PointerEvent) => ev.isTrusted && live.current.move(ev);\n    const onUp = (ev: PointerEvent) => ev.isTrusted && live.current.up(ev);\n    window.addEventListener('pointermove', onMove);\n    window.addEventListener('pointerup', onUp);\n    window.addEventListener('pointercancel', onUp);\n    unwatch.current = () => {\n      window.removeEventListener('pointermove', onMove);\n      window.removeEventListener('pointerup', onUp);\n      window.removeEventListener('pointercancel', onUp);\n      unwatch.current = null;\n    };\n  };\n\n  const move = (e: MoveEvent) => {\n    const g = grip.current;\n    if (!g || g.id !== e.pointerId) return;\n    const at = local(e.clientX);\n    if (g.grab === null) {\n      g.grab = at - x.get();\n      return;\n    }\n    const next = clamp(at - g.grab, 0, TRAVEL);\n    if (Math.abs(next - x.get()) > 0.5) g.moved = true;\n    g.hist.push([e.timeStamp, next]);\n    if (g.hist.length > 4) g.hist.shift();\n    x.set(next);\n  };\n\n  const up = (e: UpEvent) => {\n    const g = grip.current;\n    if (!g || g.id !== e.pointerId) return;\n    grip.current = null;\n    unwatch.current?.();\n    try {\n      trackRef.current?.releasePointerCapture(e.pointerId);\n    } catch {}\n    setHeld(false);\n    if (x.get() >= TRAVEL) commit(false);\n    else if (g.moved) goHome(velocityOf(g.hist));\n  };\n  live.current = { move, up };\n\n  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    if (disabled || phase === 'pending' || phase === 'done') return;\n    const step = TRAVEL / 10;\n    if (e.key === 'End') {\n      e.preventDefault();\n      commit(true);\n    } else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\n      e.preventDefault();\n      const next = Math.min(TRAVEL, x.get() + step);\n      x.set(next);\n      if (next >= TRAVEL) commit(true);\n    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\n      e.preventDefault();\n      x.set(Math.max(0, x.get() - step));\n    } else if (e.key === 'Home' || e.key === 'Escape') {\n      e.preventDefault();\n      if (grip.current) up({ pointerId: grip.current.id });\n      else x.set(0);\n    }\n  };\n\n  const fontSize = clamp(Math.round(height * 0.25), 13, 17);\n  const iconSize = Math.round(GRIP * 0.42);\n  const done = phase === 'done';\n\n  return (\n    <div\n      className={`sc-root group relative inline-block align-middle [font-family:inherit] data-[disabled]:pointer-events-none data-[disabled]:opacity-[0.55]${className ? ` ${className}` : ''}`}\n      data-phase={phase}\n      data-held={held ? '' : undefined}\n      data-disabled={disabled ? '' : undefined}\n      style={\n        {\n          width,\n          height,\n          '--sc-track': trackColor,\n          '--sc-ink': handleColor,\n          '--sc-ok': successColor,\n          '--sc-no': dangerColor,\n          '--sc-on-ink': onColor(handleColor),\n          '--sc-on-ok': onColor(successColor),\n          '--sc-on-no': onColor(dangerColor),\n          '--sc-radius': `${r}px`,\n          '--sc-grip-r': `${gripR}px`,\n          '--sc-pad': `${PAD}px`,\n          '--sc-font': `${fontSize}px`\n        } as CSSProperties\n      }\n    >\n      <style>{STYLE}</style>\n      <motion.div\n        ref={trackRef}\n        className=\"relative h-full w-full cursor-grab touch-none select-none [-webkit-touch-callout:none] [-webkit-tap-highlight-color:transparent] rounded-[var(--sc-radius)] [background:var(--sc-track)] group-data-[held]:cursor-grabbing group-data-[phase=pending]:cursor-default group-data-[phase=done]:cursor-default\"\n        style={{ transform: trackTransform }}\n        onPointerDown={down}\n      >\n        <motion.span\n          className=\"pointer-events-none absolute inset-0 grid place-items-center whitespace-nowrap font-medium leading-none tracking-[-0.006em] [font-size:var(--sc-font)]\"\n          style={{ opacity: say }}\n          aria-hidden=\"true\"\n        >\n          <span className=\"[grid-area:1/1] [transition:opacity_200ms_ease,filter_200ms_ease] [color:color-mix(in_srgb,var(--sc-ink)_45%,transparent)] group-data-[phase=error]:opacity-0 group-data-[phase=error]:blur-[2px]\">\n            {label}\n          </span>\n          <span className=\"[grid-area:1/1] [transition:opacity_200ms_ease,filter_200ms_ease] [color:var(--sc-no)] opacity-0 blur-[2px] group-data-[phase=error]:opacity-100 group-data-[phase=error]:blur-none\">\n            {errorLabel}\n          </span>\n        </motion.span>\n        <motion.div\n          ref={capsuleRef}\n          role=\"slider\"\n          tabIndex={disabled ? -1 : 0}\n          aria-label={labelText}\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={0}\n          aria-busy={phase === 'pending' || undefined}\n          aria-disabled={disabled || undefined}\n          className=\"absolute top-[var(--sc-pad)] left-[var(--sc-pad)] h-[calc(100%-var(--sc-pad)*2)] w-[calc(100%-var(--sc-pad)*2)] outline-none [background:var(--sc-ink)] [color:var(--sc-on-ink)] [transition:background-color_200ms_ease,color_200ms_ease] group-data-[phase=done]:[background:var(--sc-ok)] group-data-[phase=done]:[color:var(--sc-on-ok)] group-data-[phase=error]:[background:var(--sc-no)] group-data-[phase=error]:[color:var(--sc-on-no)] focus-visible:[box-shadow:inset_0_0_0_2px_var(--sc-track)]\"\n          style={{ clipPath: clip, transform: shape, transformOrigin: origin }}\n          onPointerEnter={e => {\n            if (e.pointerType === 'mouse' && finePointer()) setHot(true);\n          }}\n          onPointerLeave={() => setHot(false)}\n          onKeyDown={onKeyDown}\n        >\n          <motion.div className=\"absolute inset-0\" style={{ transform: content }}>\n            <motion.span\n              className=\"pointer-events-none absolute inset-0 flex items-center justify-center gap-2 whitespace-nowrap font-semibold leading-none tracking-[-0.006em] [font-size:var(--sc-font)] [transition:filter_200ms_ease] group-data-[phase=pending]:blur-[2px] [&>svg]:block\"\n              style={{ opacity: arrow }}\n              aria-hidden=\"true\"\n            >\n              {icon ?? <HugeiconsIcon icon={ArrowRight02Icon} size={iconSize} strokeWidth={2} />}\n            </motion.span>\n            <motion.span\n              className=\"pointer-events-none absolute inset-0 flex items-center justify-center gap-2 whitespace-nowrap font-semibold leading-none tracking-[-0.006em] [font-size:var(--sc-font)] [transition:filter_200ms_ease] blur-[2px] group-data-[phase=pending]:blur-none\"\n              style={{ opacity: spin }}\n              aria-hidden=\"true\"\n            >\n              <Spinner size={iconSize} />\n            </motion.span>\n            <motion.span\n              className=\"pointer-events-none absolute inset-0 flex items-center justify-center gap-2 whitespace-nowrap font-semibold leading-none tracking-[-0.006em] [font-size:var(--sc-font)] [&>svg]:block\"\n              aria-hidden=\"true\"\n              initial={false}\n              animate={{ opacity: done ? 1 : 0, scale: done || reduce ? 1 : 0.95 }}\n              transition={{ duration: 0.2, ease: EASE_OUT }}\n            >\n              <HugeiconsIcon icon={Tick02Icon} size={Math.round(GRIP * 0.38)} strokeWidth={2.5} />\n              {doneLabel}\n            </motion.span>\n          </motion.div>\n        </motion.div>\n        <span className=\"sr-only\" aria-live=\"polite\">\n          {phase === 'pending' ? 'Working' : phase === 'done' ? doneLabel : phase === 'error' ? errorLabel : ''}\n        </span>\n      </motion.div>\n    </div>\n  );\n};\n\nexport default SlideCommit;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12",
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}