{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cart-routine-grid",
  "type": "registry:component",
  "title": "Complete the routine",
  "description": "Three numbered routine steps for the cart page; each explains its role, steps in the cart show as included, add the rest in one click.",
  "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/button.json",
    "https://storekit-eight.vercel.app/r/product-image.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "cart/cart-routine-grid.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Check, Plus } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { ProductImage } from \"@/components/storekit/ui/product-image\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* \"Complete the routine\" grid for the cart page. Three numbered steps that\n   explain where each product fits, so the add-on reads as the next step and\n   not a random recommendation. Steps already in the cart show as included. */\n\nexport type RoutineStep = { id: string; step: string; title: string; purpose: string; price: number; image: string; inCart?: boolean }\n\nconst SAMPLE: RoutineStep[] = [\n  { id: \"daily-greens\", step: \"Morning\", title: \"Daily Greens\", purpose: \"One scoop of greens before coffee.\", price: 4900, image: \"/brands/morrow/greens.webp\", inCart: true },\n  { id: \"electrolytes\", step: \"Midday\", title: \"Electrolytes\", purpose: \"Salt and potassium for training days and long afternoons.\", price: 3200, image: \"/brands/morrow/electrolytes.webp\" },\n  { id: \"magnesium-night\", step: \"Night\", title: \"Magnesium Night\", purpose: \"Two gummies to wind down before bed.\", price: 2900, image: \"/brands/morrow/magnesium.webp\" },\n]\n\nexport function CartRoutineGrid({\n  title = \"Complete the routine\",\n  steps = SAMPLE,\n  onAdd,\n  className,\n}: {\n  title?: string\n  steps?: RoutineStep[]\n  onAdd?: (ids: string[]) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"cart\", \"cart-routine-grid\")\n  const [inCart, setInCart] = React.useState<Record<string, boolean>>(() => Object.fromEntries(steps.map((s) => [s.id, !!s.inCart])))\n  const missing = steps.filter((s) => !inCart[s.id])\n  const missingTotal = missing.reduce((n, s) => n + s.price, 0)\n  const titleId = React.useId()\n\n  const add = (ids: string[]) => {\n    if (!ids.length) return\n    setInCart((m) => ({ ...m, ...Object.fromEntries(ids.map((id) => [id, true])) }))\n    sk.emit(\"convert\", ids.length > 1 ? \"routine_add_all\" : \"routine_add\", steps.filter((s) => ids.includes(s.id)).reduce((n, s) => n + s.price, 0))\n    onAdd?.(ids)\n  }\n\n  return (\n    <section ref={sk.ref as React.Ref<HTMLElement>} {...sk.attrs} aria-labelledby={titleId} className={cn(\"w-full\", className)}>\n      <div className=\"flex flex-wrap items-end justify-between gap-3\">\n        <div>\n          <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">\n            <span className=\"tabular-nums\">{steps.length - missing.length}</span> of <span className=\"tabular-nums\">{steps.length}</span> in your cart\n          </p>\n          <h3 id={titleId} className=\"mt-1 font-sk-display text-xl font-semibold leading-[1.1] tracking-[-0.02em]\">\n            {title}\n          </h3>\n        </div>\n        {missing.length > 1 ? (\n          <Button size=\"sm\" variant=\"outline\" className=\"active:scale-[0.97]\" onClick={() => add(missing.map((s) => s.id))}>\n            Add the remaining {missing.length} <span className=\"tabular-nums\">{money(missingTotal)}</span>\n          </Button>\n        ) : null}\n      </div>\n\n      <ol className=\"mt-4 grid gap-3 sm:grid-cols-3\">\n        {steps.map((s, i) => {\n          const on = inCart[s.id]\n          return (\n            <li key={s.id} className={cn(\"flex gap-3 rounded-sk-card border border-sk-line bg-sk-card p-3 sm:flex-col sm:p-4\")}>\n              <ProductImage src={s.image} alt={s.title} className=\"h-24 w-24 shrink-0 rounded-sk-card sm:h-auto sm:w-full\" zoomOnHover={false} />\n              <div className=\"flex min-w-0 flex-1 flex-col\">\n                <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">\n                  Step <span className=\"tabular-nums\">{i + 1}</span> / {s.step}\n                </p>\n                <p className=\"mt-1 truncate text-[15px] font-medium text-sk-ink\">{s.title}</p>\n                <p className=\"mt-0.5 text-[13px] leading-[1.5] text-sk-muted\">{s.purpose}</p>\n                <div className=\"mt-auto flex items-center justify-between gap-2 pt-3\">\n                  <span className=\"text-sm font-semibold tabular-nums text-sk-price\">{money(s.price)}</span>\n                  <button\n                    type=\"button\"\n                    disabled={on}\n                    onClick={() => add([s.id])}\n                    className={cn(\n                      \"inline-flex h-10 items-center gap-1.5 rounded-sk-btn px-3 text-[13px] font-medium transition-[background-color,color,transform] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-card active:scale-[0.97]\",\n                      on ? \"cursor-default bg-sk-savings/15 text-sk-savings\" : \"bg-sk-accent text-sk-accent-ink hover:brightness-[1.06]\"\n                    )}\n                  >\n                    <AnimatePresence mode=\"popLayout\" initial={false}>\n                      <motion.span\n                        key={on ? \"in\" : \"add\"}\n                        className=\"inline-flex items-center gap-1.5\"\n                        initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}\n                        transition={{ duration: reduce ? 0 : 0.2 }}\n                      >\n                        {on ? <Check size={14} /> : <Plus size={14} />}\n                        {on ? \"In cart\" : \"Add\"}\n                      </motion.span>\n                    </AnimatePresence>\n                  </button>\n                </div>\n              </div>\n            </li>\n          )\n        })}\n      </ol>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/cart/cart-routine-grid.tsx"
    }
  ],
  "meta": {
    "section": "cart",
    "variant": "grid",
    "niche": [
      "any"
    ],
    "commerce": [
      "upsell",
      "bundle",
      "atc"
    ],
    "previewHeight": 520
  }
}