{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slide-button",
  "title": "Slide Button",
  "description": "Slide-to-confirm track with a draggable knob, shimmering label and keyboard support.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/slide-button/slide-button.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface SlideButtonProps {\n  /** Text shown on the track. Default: \"Slide to confirm\" */\n  label?: string\n  /** Text shown once completed. Default: \"Confirmed\" */\n  completeLabel?: string\n  /** Fraction of the track the knob must pass to complete, 0 to 1. Default: 0.9 */\n  threshold?: number\n  /** Track width in px. Default: 280 */\n  width?: number\n  /** Color of the knob and the trailing fill. Default: \"var(--foreground)\" */\n  accentColor?: string\n  /** Return to the start this many ms after completing (0 = stay complete). Default: 2000 */\n  resetAfter?: number\n  /** Called when the knob reaches the end. */\n  onComplete?: () => void\n  /** Disable interaction. Default: false */\n  disabled?: boolean\n  className?: string\n}\n\nconst KNOB = 44\nconst PAD = 4\n\nconst Check = () => (\n  <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.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.1 }}\n    />\n  </svg>\n)\n\nconst Chevrons = () => (\n  <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"m4 4 4 4-4 4M9 4l4 4-4 4\" />\n  </svg>\n)\n\nexport function SlideButton({\n  label = \"Slide to confirm\",\n  completeLabel = \"Confirmed\",\n  threshold = 0.9,\n  width = 280,\n  accentColor = \"var(--foreground)\",\n  resetAfter = 2000,\n  onComplete,\n  disabled = false,\n  className,\n}: SlideButtonProps) {\n  const reduced = useReducedMotion()\n  const [done, setDone] = useState(false)\n  const [percent, setPercent] = useState(0)\n  const resetTimer = useRef<ReturnType<typeof setTimeout>>(undefined)\n\n  const max = Math.max(width - KNOB - PAD * 2, 1)\n  const x = useMotionValue(0)\n  const fill = useTransform(x, (v) => v + KNOB + PAD * 2)\n  const labelOpacity = useTransform(x, [0, max * 0.6], [1, 0])\n  const labelX = useTransform(x, [0, max], [0, 24])\n\n  useMotionValueEvent(x, \"change\", (v) => setPercent(Math.round((v / max) * 100)))\n  useEffect(() => () => clearTimeout(resetTimer.current), [])\n\n  const spring = reduced ? { duration: 0 } : { type: \"spring\" as const, stiffness: 420, damping: 36 }\n\n  const complete = () => {\n    setDone(true)\n    animate(x, max, spring)\n    onComplete?.()\n    if (resetAfter > 0) {\n      resetTimer.current = setTimeout(() => {\n        setDone(false)\n        animate(x, 0, spring)\n      }, resetAfter)\n    }\n  }\n\n  const settle = () => {\n    if (x.get() >= max * threshold) complete()\n    else animate(x, 0, spring)\n  }\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (done || disabled) return\n    const step = max * 0.1\n    let target: number | null = null\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") target = Math.min(x.get() + step, max)\n    else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") target = Math.max(x.get() - step, 0)\n    else if (e.key === \"Home\") target = 0\n    else if (e.key === \"End\" || e.key === \"Enter\") target = max\n    if (target === null) return\n    e.preventDefault()\n    if (target >= max * threshold) complete()\n    else animate(x, target, spring)\n  }\n\n  return (\n    <div\n      className={cn(\n        \"relative flex h-[52px] select-none items-center overflow-hidden rounded-full border bg-muted\",\n        disabled && \"pointer-events-none opacity-50\",\n        className\n      )}\n      style={{ width, padding: PAD }}\n    >\n      {/* trailing fill */}\n      <motion.div\n        aria-hidden=\"true\"\n        className=\"absolute inset-y-0 left-0 rounded-full opacity-15\"\n        style={{ width: fill, background: accentColor }}\n      />\n\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {done ? (\n          <motion.span\n            key=\"done\"\n            className=\"pointer-events-none absolute inset-0 flex items-center justify-center pr-12 text-sm font-medium text-foreground\"\n            initial={{ opacity: 0, y: reduced ? 0 : 8 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0 }}\n          >\n            {completeLabel}\n          </motion.span>\n        ) : (\n          <motion.span\n            key=\"label\"\n            className=\"pointer-events-none absolute inset-0 flex items-center justify-center pl-12 text-sm font-medium\"\n            style={{ opacity: labelOpacity, x: labelX }}\n            exit={{ opacity: 0 }}\n          >\n            <span className=\"relative text-muted-foreground\">\n              {label}\n              {/* Solid foreground band revealed through a sliding clip-path */}\n              {!reduced && (\n                <motion.span\n                  aria-hidden=\"true\"\n                  className=\"absolute inset-0 text-foreground\"\n                  initial={{ clipPath: \"inset(0% 100% 0% 0%)\" }}\n                  animate={{\n                    clipPath: [\"inset(0% 100% 0% 0%)\", \"inset(0% 80% 0% 0%)\", \"inset(0% 0% 0% 80%)\", \"inset(0% 0% 0% 100%)\"],\n                  }}\n                  transition={{ duration: 2.4, times: [0, 0.12, 0.88, 1], repeat: Infinity, ease: \"linear\" }}\n                >\n                  {label}\n                </motion.span>\n              )}\n            </span>\n          </motion.span>\n        )}\n      </AnimatePresence>\n\n      <motion.div\n        role=\"slider\"\n        tabIndex={disabled ? -1 : 0}\n        aria-label={label}\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuenow={percent}\n        aria-valuetext={done ? completeLabel : `${percent}%`}\n        aria-disabled={disabled || undefined}\n        drag={done || disabled ? false : \"x\"}\n        dragConstraints={{ left: 0, right: max }}\n        dragElastic={0.04}\n        dragMomentum={false}\n        onDragEnd={settle}\n        onKeyDown={onKeyDown}\n        whileTap={done ? undefined : { scale: 0.94 }}\n        className=\"relative z-10 grid cursor-grab touch-none place-items-center rounded-full text-background shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-muted active:cursor-grabbing\"\n        style={{ x, width: KNOB, height: KNOB, background: accentColor }}\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={done ? \"check\" : \"arrow\"}\n            initial={{ opacity: 0, scale: 0.5 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.5 }}\n            transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n          >\n            {done ? <Check /> : <Chevrons />}\n          </motion.span>\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/slide-button",
  "type": "registry:component"
}