{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cart-points",
  "type": "registry:component",
  "title": "Loyalty points",
  "description": "Points balance and value, points earned on this order, and a redeem switch in fixed blocks.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "cart",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "cart/cart-points.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Sparkles } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Loyalty points in the cart. Shows the balance and its cash value, the\n   points this order will earn, and a switch to redeem in fixed blocks.\n   Earned points recompute on the amount actually paid. */\n\nexport function CartPoints({\n  balance = 1240,\n  subtotal = 14800,\n  pointsPerDollar = 1,\n  centsPerPoint = 1,\n  redeemStep = 500,\n  defaultRedeem = false,\n  onChange,\n  className,\n}: {\n  balance?: number\n  /** In cents. */\n  subtotal?: number\n  pointsPerDollar?: number\n  /** Value of one point in cents. */\n  centsPerPoint?: number\n  /** Points redeem in blocks of this size. */\n  redeemStep?: number\n  defaultRedeem?: boolean\n  onChange?: (v: { redeem: boolean; pointsUsed: number; discount: number }) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"cart\", \"cart-points\")\n  const [redeem, setRedeem] = React.useState(defaultRedeem)\n  const maxBySubtotal = Math.floor(subtotal / centsPerPoint)\n  const usable = Math.floor(Math.min(balance, maxBySubtotal) / redeemStep) * redeemStep\n  const canRedeem = usable > 0\n  const on = redeem && canRedeem\n  const discount = on ? usable * centsPerPoint : 0\n  const paid = subtotal - discount\n  const earned = Math.floor((paid / 100) * pointsPerDollar)\n  const fmt = (n: number) => n.toLocaleString(\"en-US\")\n\n  const toggle = () => {\n    if (!canRedeem) return\n    const next = !redeem\n    setRedeem(next)\n    onChange?.({ redeem: next, pointsUsed: next ? usable : 0, discount: next ? usable * centsPerPoint : 0 })\n    sk.emit(next ? \"convert\" : \"interact\", next ? \"points_redeem\" : \"points_unredeem\", usable * centsPerPoint)\n  }\n\n  return (\n    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn(\"w-full rounded-sk-card border border-sk-line bg-sk-card\", className)}>\n      <div className=\"grid grid-cols-2 divide-x divide-sk-line border-b border-sk-line\">\n        <div className=\"p-4\">\n          <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">Balance</p>\n          <p className=\"mt-1 font-sk-display text-xl font-semibold tabular-nums tracking-[-0.02em]\">{fmt(on ? balance - usable : balance)}</p>\n          <p className=\"text-xs tabular-nums text-sk-muted\">Worth {money((on ? balance - usable : balance) * centsPerPoint)}</p>\n        </div>\n        <div className=\"p-4\">\n          <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">This order earns</p>\n          <p className=\"mt-1 flex items-center gap-1.5 font-sk-display text-xl font-semibold tabular-nums tracking-[-0.02em] text-sk-savings\">\n            <Sparkles size={15} />\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={earned}\n                initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8 }}\n                transition={{ duration: reduce ? 0 : 0.3, ease: [0.16, 1, 0.3, 1] }}\n              >\n                +{fmt(earned)}\n              </motion.span>\n            </AnimatePresence>\n          </p>\n          <p className=\"text-xs text-sk-muted\">Added after it ships</p>\n        </div>\n      </div>\n      <button\n        type=\"button\"\n        role=\"switch\"\n        aria-checked={on}\n        disabled={!canRedeem}\n        onClick={toggle}\n        className=\"flex min-h-[60px] w-full items-center gap-3 rounded-b-sk-card px-4 py-3 text-left transition-colors duration-200 hover:bg-sk-ink/[0.03] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sk-accent active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-transparent\"\n      >\n        <span className=\"min-w-0 flex-1\">\n          <span className=\"block text-sm font-medium text-sk-ink\">\n            {canRedeem ? (\n              <>\n                Use <span className=\"tabular-nums\">{fmt(usable)}</span> points for <span className=\"tabular-nums\">{money(usable * centsPerPoint)}</span> off\n              </>\n            ) : (\n              <>Redeem from {fmt(redeemStep)} points</>\n            )}\n          </span>\n          <span className=\"block text-[13px] text-sk-muted\">\n            {canRedeem ? `Redeems in blocks of ${fmt(redeemStep)}.` : `You need ${fmt(redeemStep - balance)} more to redeem.`}\n          </span>\n        </span>\n        <span aria-hidden className={cn(\"relative flex h-6 w-10 shrink-0 items-center rounded-sk-chip p-0.5 transition-colors duration-200\", on ? \"bg-sk-accent\" : \"bg-sk-ink/20\")}>\n          <motion.span\n            initial={false}\n            animate={{ x: on ? 16 : 0 }}\n            transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 600, damping: 34 }}\n            className={cn(\"h-5 w-5 rounded-sk-chip shadow-[0_1px_2px_rgb(0_0_0/0.25)]\", on ? \"bg-sk-accent-ink\" : \"bg-sk-paper\")}\n          />\n        </span>\n      </button>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/cart/cart-points.tsx"
    }
  ],
  "meta": {
    "section": "cart",
    "variant": "loyalty",
    "niche": [
      "any"
    ],
    "commerce": [
      "loyalty",
      "discount"
    ],
    "previewHeight": 340
  }
}