{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upsell-bulk-tiers",
  "type": "registry:component",
  "title": "Bulk rate table",
  "description": "Stock-up pricing by quantity band. The matching row highlights as the stepper changes.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "upsell",
    "supplements",
    "wellness"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/stepper.json"
  ],
  "files": [
    {
      "path": "upsell/upsell-bulk-tiers.tsx",
      "content": "\"use client\"\n\nimport { useId, useState } from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { Stepper } from \"@/components/storekit/ui/stepper\"\n\n/* Stock-up pricing. The row that matches the current quantity is\n   highlighted by a bar that slides with a shared layoutId. Clicking a row\n   jumps the quantity to that tier. */\n\ntype Tier = { min: number; max?: number; discount: number }\n\nconst EASE = [0.23, 1, 0.32, 1] as const\nconst RING = \"outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-inset\"\n\nconst DEFAULT_TIERS: Tier[] = [\n  { min: 1, max: 4, discount: 0 },\n  { min: 5, max: 9, discount: 0.08 },\n  { min: 10, max: 24, discount: 0.15 },\n  { min: 25, discount: 0.22 },\n]\n\nexport function UpsellBulkTiers({\n  unitPrice = 5900,\n  tiers = DEFAULT_TIERS,\n  defaultQty = 6,\n  title = \"Stock up and save\",\n  onChange,\n  className,\n}: {\n  unitPrice?: number\n  tiers?: Tier[]\n  defaultQty?: number\n  title?: string\n  onChange?: (qty: number, unit: number) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const uid = useId()\n  const [qty, setQty] = useState(defaultQty)\n  const activeIndex = tiers.findIndex((t) => qty >= t.min && (t.max === undefined || qty <= t.max))\n  const active = tiers[activeIndex] ?? tiers[0]\n  const unit = Math.round(unitPrice * (1 - active.discount))\n\n  const update = (n: number) => {\n    setQty(n)\n    const t = tiers.find((x) => n >= x.min && (x.max === undefined || n <= x.max)) ?? tiers[0]\n    onChange?.(n, Math.round(unitPrice * (1 - t.discount)))\n  }\n\n  return (\n    <div data-sk-section=\"upsell\" data-sk-variant=\"upsell-bulk-tiers\" className={cn(\"w-full rounded-sk-card border border-sk-line bg-sk-card p-5 sm:p-6\", className)}>\n      <div className=\"flex items-baseline justify-between gap-4\">\n        <h3 className=\"min-w-0 font-sk-display text-lg font-semibold leading-[1.1] tracking-[-0.02em] text-sk-ink\">{title}</h3>\n        <span className=\"shrink-0 whitespace-nowrap font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">Per unit</span>\n      </div>\n\n      <ul className=\"mt-4 flex flex-col\" aria-label=\"Bulk price tiers\">\n        <li className=\"grid grid-cols-[1fr_auto_auto] gap-3 px-3 pb-2 font-sk-mono text-[11px] uppercase tracking-[0.08em] text-sk-muted\" aria-hidden>\n          <span>Units</span>\n          <span className=\"w-20 text-right\">Price</span>\n          <span className=\"w-12 text-right\">Save</span>\n        </li>\n        {tiers.map((t, i) => {\n          const on = i === activeIndex\n          const price = Math.round(unitPrice * (1 - t.discount))\n          return (\n            <li key={t.min}>\n              <button\n                type=\"button\"\n                aria-pressed={on}\n                onClick={() => update(t.min)}\n                className={cn(\"relative grid min-h-[44px] w-full grid-cols-[1fr_auto_auto] items-center gap-4 rounded-sk-btn px-3 text-left text-sm transition-colors duration-200 hover:bg-sk-ink/[0.03]\", RING)}\n              >\n                {on ? (\n                  <motion.span\n                    layoutId={`${uid}-bar`}\n                    aria-hidden\n                    transition={{ duration: reduce ? 0 : 0.4, ease: EASE }}\n                    className=\"absolute inset-0 rounded-sk-btn border border-sk-accent/40 bg-sk-accent/[0.08]\"\n                  />\n                ) : null}\n                <span className={cn(\"relative whitespace-nowrap font-medium tabular-nums\", on ? \"text-sk-ink\" : \"text-sk-muted\")}>\n                  {t.max === undefined ? `${t.min}+` : `${t.min} to ${t.max}`}\n                </span>\n                <span className=\"relative w-20 text-right font-semibold tabular-nums text-sk-price\">{money(price)}</span>\n                <span className={cn(\"relative w-12 text-right font-sk-mono text-[12px] tabular-nums\", t.discount > 0 ? \"text-sk-savings\" : \"text-sk-muted\")}>\n                  {t.discount > 0 ? `${Math.round(t.discount * 100)}%` : \"None\"}\n                </span>\n              </button>\n            </li>\n          )\n        })}\n      </ul>\n\n      <div className=\"mt-5 flex flex-wrap items-center justify-between gap-4 border-t border-sk-line pt-5\">\n        <Stepper value={qty} onChange={update} max={200} />\n        <div className=\"flex flex-col items-end\" aria-live=\"polite\">\n          <span className=\"text-[13px] tabular-nums text-sk-muted\">\n            {qty} x {money(unit)}\n          </span>\n          <span className=\"text-xl font-semibold tabular-nums text-sk-price\">{money(unit * qty)}</span>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/upsell/upsell-bulk-tiers.tsx"
    }
  ],
  "meta": {
    "section": "upsell",
    "variant": "bulk",
    "niche": [
      "supplements",
      "wellness"
    ],
    "commerce": [
      "quantity",
      "bundle"
    ],
    "previewHeight": 440
  }
}