{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upsell-express-row",
  "type": "registry:block",
  "title": "Add to cart with express row",
  "description": "Add to cart stays primary; express wallets sit beneath as a quieter row of text wordmarks.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "upsell",
    "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/sk-analytics.json"
  ],
  "files": [
    {
      "path": "upsell/upsell-express-row.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Check, Lock } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Add to cart stays the primary action; express wallets sit beneath it as a\n   quieter row so they do not compete with bundles or the cart. Wallets are\n   plain text wordmarks in theme tokens. Swap in each provider's official\n   button when you wire the real SDK. Amounts are integer cents. */\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-offset-2 focus-visible:ring-offset-sk-paper\"\n\ntype Wallet = { id: string; label: string }\n\nconst DEFAULT_WALLETS: Wallet[] = [\n  { id: \"shop\", label: \"Shop Pay\" },\n  { id: \"apple\", label: \"Apple Pay\" },\n  { id: \"google\", label: \"Google Pay\" },\n  { id: \"paypal\", label: \"PayPal\" },\n]\n\nexport function UpsellExpressRow({\n  total = 5900,\n  wallets = DEFAULT_WALLETS,\n  onAddToCart,\n  onExpress,\n  className,\n}: {\n  total?: number\n  wallets?: Wallet[]\n  onAddToCart?: () => void\n  /** Called with the wallet id. Hand off to the provider's checkout here. */\n  onExpress?: (walletId: string) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"upsell\", \"upsell-express-row\")\n  const [status, setStatus] = useState<\"idle\" | \"loading\" | \"added\">(\"idle\")\n  const [pending, setPending] = useState<string | null>(null)\n  const timers = useRef<number[]>([])\n  useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), [])\n\n  const add = () => {\n    if (status !== \"idle\") return\n    setStatus(\"loading\")\n    sk.emit(\"convert\", \"add_to_cart\", total)\n    timers.current.push(\n      window.setTimeout(() => {\n        setStatus(\"added\")\n        onAddToCart?.()\n        timers.current.push(window.setTimeout(() => setStatus(\"idle\"), 1200))\n      }, 700)\n    )\n  }\n\n  const express = (id: string) => {\n    if (pending) return\n    setPending(id)\n    sk.emit(\"convert\", `express_${id}`, total)\n    onExpress?.(id)\n    timers.current.push(window.setTimeout(() => setPending(null), 1200))\n  }\n\n  return (\n    <div {...sk.attrs} ref={sk.ref as React.RefObject<HTMLDivElement>} className={cn(\"flex w-full flex-col gap-3\", className)}>\n      <Button size=\"lg\" className=\"w-full active:scale-[0.97]\" loading={status === \"loading\"} onClick={add}>\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.span\n            key={status === \"added\" ? \"added\" : \"add\"}\n            initial={{ opacity: 0, y: reduce ? 0 : 6 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: reduce ? 0 : -6 }}\n            transition={{ duration: reduce ? 0 : 0.2, ease: EASE }}\n            className=\"inline-flex items-center gap-2 tabular-nums\"\n          >\n            {status === \"added\" ? (\n              <>\n                <Check size={16} strokeWidth={2.5} /> Added to cart\n              </>\n            ) : (\n              <>Add to cart · {money(total)}</>\n            )}\n          </motion.span>\n        </AnimatePresence>\n      </Button>\n\n      <div className=\"flex items-center gap-3 text-[12px] text-sk-muted\" aria-hidden>\n        <span className=\"h-px flex-1 bg-sk-line\" />\n        or check out now with\n        <span className=\"h-px flex-1 bg-sk-line\" />\n      </div>\n\n      <div role=\"group\" aria-label=\"Express checkout\" className={cn(\"grid gap-2\", wallets.length >= 4 ? \"grid-cols-2 sm:grid-cols-4\" : wallets.length === 3 ? \"grid-cols-3\" : \"grid-cols-2\")}>\n        {wallets.map((w) => {\n          const busy = pending === w.id\n          return (\n            <button\n              key={w.id}\n              type=\"button\"\n              aria-label={`Check out with ${w.label}`}\n              aria-busy={busy || undefined}\n              disabled={!!pending && !busy}\n              onClick={() => express(w.id)}\n              className={cn(\n                \"relative h-11 rounded-sk-btn border border-sk-line bg-sk-card px-3 font-sk-display text-[15px] font-semibold tracking-[-0.01em] text-sk-ink transition-[border-color,background-color,opacity,transform] duration-200 hover:border-sk-ink/40 hover:bg-sk-ink/[0.03] active:scale-[0.97] disabled:opacity-50\",\n                RING\n              )}\n            >\n              <span className={cn(\"transition-opacity duration-200\", busy && \"opacity-0\")}>{w.label}</span>\n              {busy ? (\n                <span className=\"absolute inset-0 grid place-items-center\">\n                  <span className=\"h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent motion-reduce:animate-none\" />\n                </span>\n              ) : null}\n            </button>\n          )\n        })}\n      </div>\n\n      <p className=\"flex items-center justify-center gap-1.5 text-[12px] text-sk-muted\">\n        <Lock size={12} aria-hidden /> Express checkout skips the cart and uses your saved details.\n      </p>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/upsell/upsell-express-row.tsx"
    }
  ],
  "meta": {
    "section": "upsell",
    "variant": "express-row",
    "niche": [
      "any"
    ],
    "commerce": [
      "atc",
      "express"
    ],
    "previewHeight": 300
  }
}