{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "add-to-cart-button",
  "type": "registry:ui",
  "title": "Add To Cart Button",
  "description": "A morphing add-to-cart button with spring physics, slot-machine quantity flip, and live price updates.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/add-to-cart-button.tsx",
      "content": "\"use client\";\n\nimport { motion, AnimatePresence, type Transition } from \"motion/react\";\nimport { useState } from \"react\";\n\n/**\n * Add-to-cart morphing button — Rauno Freiberg style.\n *\n * Design rules applied:\n *   1. Monochrome — neutral-900 / white / one muted accent\n *   2. Spring physics everywhere — no cubic-bezier, real springs\n *   3. One morphing container — layout animates via spring\n *   4. Content crossfades — whole phrase, not per-character\n *   5. Only transform + opacity — zero filter:blur\n *   6. Fewer elements — no pills-in-pills, no checkmarks, no icons\n *   7. Slot-machine flip — the only \"flashy\" detail, earned\n *   8. Hover & press feel physical — scale springs with overshoot\n */\n\n/* ── spring presets ── */\n\nconst SPRING_LAYOUT: Transition = {\n  type: \"spring\",\n  stiffness: 400,\n  damping: 30,\n};\n\nconst SPRING_CONTENT: Transition = {\n  type: \"spring\",\n  stiffness: 300,\n  damping: 25,\n};\n\nconst SPRING_TAP: Transition = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n};\n\n/* ── component ── */\n\ninterface AddToCartButtonProps {\n  price?: number;\n  currency?: string;\n  maxQuantity?: number;\n  onQuantityChange?: (quantity: number) => void;\n}\n\nexport function AddToCartButton({\n  price = 24.99,\n  currency = \"$\",\n  maxQuantity = 99,\n  onQuantityChange,\n}: AddToCartButtonProps) {\n  const [added, setAdded] = useState(false);\n  const [qty, setQty] = useState(1);\n  const [locked, setLocked] = useState(false);\n\n  const total = `${currency}${(price * qty).toFixed(2)}`;\n\n  const lock = () => {\n    setLocked(true);\n    setTimeout(() => setLocked(false), 350);\n  };\n\n  const add = () => {\n    if (locked) return;\n    lock();\n    setAdded(true);\n    setQty(1);\n    onQuantityChange?.(1);\n  };\n\n  const minus = () => {\n    if (qty <= 1) {\n      if (locked) return;\n      lock();\n      setAdded(false);\n      onQuantityChange?.(0);\n      return;\n    }\n    const n = qty - 1;\n    setQty(n);\n    onQuantityChange?.(n);\n  };\n\n  const plus = () => {\n    const n = Math.min(qty + 1, maxQuantity);\n    setQty(n);\n    onQuantityChange?.(n);\n  };\n\n  return (\n    <div className=\"flex items-center justify-center\">\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {!added ? (\n          /* ────────────────────────────────────\n             STATE A — \"Add to cart\"\n             Quiet, confident, one surface.\n          ──────────────────────────────────── */\n          <motion.button\n            key=\"idle\"\n            layoutId=\"cart\"\n            onClick={add}\n            whileHover={{ scale: 1.02 }}\n            whileTap={{ scale: 0.97 }}\n            style={{ pointerEvents: locked ? \"none\" : \"auto\" }}\n            className=\"relative flex items-center justify-center gap-2 rounded-xl bg-neutral-900 px-5 py-3 text-[14px] font-medium text-white outline-none cursor-pointer select-none border border-white/[0.08]\"\n            transition={{\n              layout: SPRING_LAYOUT,\n              scale: SPRING_TAP,\n            }}\n          >\n            <motion.span\n              key=\"label-add\"\n              initial={{ opacity: 0, y: 6 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -6 }}\n              transition={SPRING_CONTENT}\n            >\n              Add to cart\n            </motion.span>\n          </motion.button>\n        ) : (\n          /* ────────────────────────────────────\n             STATE B — Quantity controls\n             Same surface, wider. Minimal controls.\n          ──────────────────────────────────── */\n          <motion.div\n            key=\"active\"\n            layoutId=\"cart\"\n            style={{ pointerEvents: locked ? \"none\" : \"auto\" }}\n            className=\"relative flex items-center gap-3 rounded-xl bg-neutral-900 px-4 py-2.5 text-[14px] font-medium text-white border border-white/[0.08] select-none\"\n            transition={{\n              layout: SPRING_LAYOUT,\n            }}\n          >\n            {/* ── Minus ── */}\n            <motion.button\n              onClick={minus}\n              whileHover={{ scale: 1.15 }}\n              whileTap={{ scale: 0.85 }}\n              initial={{ opacity: 0, scale: 0.6 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: 0.6 }}\n              transition={SPRING_CONTENT}\n              className=\"flex h-7 w-7 items-center justify-center rounded-lg text-neutral-400 hover:text-white hover:bg-white/[0.06] transition-colors duration-150 cursor-pointer outline-none\"\n            >\n              <span className=\"text-[16px] leading-none\">−</span>\n            </motion.button>\n\n            {/* ── Quantity — slot-machine flip ── */}\n            <motion.div\n              className=\"relative flex h-7 w-8 items-center justify-center overflow-hidden\"\n              initial={{ opacity: 0, scale: 0.6 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: 0.6 }}\n              transition={{ ...SPRING_CONTENT, delay: 0.03 }}\n            >\n              <AnimatePresence mode=\"popLayout\" initial={false}>\n                <motion.span\n                  key={qty}\n                  initial={{ y: 14, opacity: 0 }}\n                  animate={{ y: 0, opacity: 1 }}\n                  exit={{ y: -14, opacity: 0 }}\n                  transition={{\n                    type: \"spring\",\n                    stiffness: 350,\n                    damping: 28,\n                  }}\n                  className=\"absolute tabular-nums\"\n                >\n                  {qty}\n                </motion.span>\n              </AnimatePresence>\n            </motion.div>\n\n            {/* ── Plus ── */}\n            <motion.button\n              onClick={plus}\n              whileHover={{ scale: 1.15 }}\n              whileTap={{ scale: 0.85 }}\n              initial={{ opacity: 0, scale: 0.6 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: 0.6 }}\n              transition={SPRING_CONTENT}\n              className=\"flex h-7 w-7 items-center justify-center rounded-lg text-neutral-400 hover:text-white hover:bg-white/[0.06] transition-colors duration-150 cursor-pointer outline-none\"\n            >\n              <span className=\"text-[16px] leading-none\">+</span>\n            </motion.button>\n\n            {/* ── Divider ── */}\n            <motion.div\n              className=\"h-4 w-px bg-white/[0.1]\"\n              initial={{ opacity: 0, scaleY: 0 }}\n              animate={{ opacity: 1, scaleY: 1 }}\n              exit={{ opacity: 0, scaleY: 0 }}\n              transition={SPRING_CONTENT}\n            />\n\n            {/* ── Price — slot-machine flip ── */}\n            <motion.div\n              className=\"relative flex h-7 items-center justify-center overflow-hidden min-w-[60px]\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ ...SPRING_CONTENT, delay: 0.05 }}\n            >\n              <AnimatePresence mode=\"popLayout\" initial={false}>\n                <motion.span\n                  key={total}\n                  initial={{ y: 14, opacity: 0 }}\n                  animate={{ y: 0, opacity: 1 }}\n                  exit={{ y: -14, opacity: 0 }}\n                  transition={{\n                    type: \"spring\",\n                    stiffness: 350,\n                    damping: 28,\n                  }}\n                  className=\"absolute text-neutral-400 tabular-nums text-[13px]\"\n                >\n                  {total}\n                </motion.span>\n              </AnimatePresence>\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport default AddToCartButton;\n",
      "type": "registry:ui",
      "target": "components/ruixen/add-to-cart-button.tsx"
    }
  ]
}