{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upsell-financing",
  "type": "registry:component",
  "title": "Financing line",
  "description": "Or 4 interest-free payments under the price, with a modal showing the exact schedule and terms.",
  "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/price.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "upsell/upsell-financing.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { X } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { Price } from \"@/components/storekit/ui/price\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Financing micro-offer. Price, then one muted line: \"or 4 payments of $X\".\n   \"How it works\" opens a modal with the real schedule (the last payment\n   absorbs rounding so the four add up to the price exactly) and the terms in\n   plain language. Hidden below the provider's minimum. 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\nfunction schedule(total: number, n: number) {\n  const base = Math.floor(total / n)\n  return Array.from({ length: n }, (_, i) => (i === n - 1 ? total - base * (n - 1) : base))\n}\n\nexport function UpsellFinancing({\n  price = 17900,\n  compareAt,\n  installments = 4,\n  intervalWeeks = 2,\n  provider = \"Pay in 4\",\n  minimum = 3500,\n  maximum = 150000,\n  terms = [\n    \"0% interest and no fees when you pay on time.\",\n    \"A soft credit check at checkout. It does not affect your score.\",\n    \"Approval and late fees depend on the provider and your state.\",\n    \"Returns refund your remaining payments first.\",\n  ],\n  className,\n}: {\n  price?: number\n  compareAt?: number\n  installments?: number\n  intervalWeeks?: number\n  /** Plain-text provider name shown in the line and modal. */\n  provider?: string\n  /** Outside this range the line is hidden, since the offer is not available. */\n  minimum?: number\n  maximum?: number\n  terms?: string[]\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const uid = useId()\n  const sk = useSkBlock(\"upsell\", \"upsell-financing\")\n  const [open, setOpen] = useState(false)\n  const triggerRef = useRef<HTMLButtonElement>(null)\n  const panelRef = useRef<HTMLDivElement>(null)\n  const closeRef = useRef<HTMLButtonElement>(null)\n  const eligible = price >= minimum && price <= maximum\n  const parts = schedule(price, installments)\n\n  useEffect(() => {\n    if (!open) return\n    const prev = document.body.style.overflow\n    document.body.style.overflow = \"hidden\"\n    const id = window.requestAnimationFrame(() => closeRef.current?.focus())\n    return () => {\n      document.body.style.overflow = prev\n      window.cancelAnimationFrame(id)\n    }\n  }, [open])\n\n  const close = () => {\n    setOpen(false)\n    triggerRef.current?.focus()\n  }\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Escape\") {\n      e.stopPropagation()\n      close()\n      return\n    }\n    if (e.key !== \"Tab\" || !panelRef.current) return\n    const els = panelRef.current.querySelectorAll<HTMLElement>(\"button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])\")\n    if (!els.length) return\n    const first = els[0]\n    const last = els[els.length - 1]\n    if (e.shiftKey && document.activeElement === first) {\n      e.preventDefault()\n      last.focus()\n    } else if (!e.shiftKey && document.activeElement === last) {\n      e.preventDefault()\n      first.focus()\n    }\n  }\n\n  return (\n    <div {...sk.attrs} ref={sk.ref as React.RefObject<HTMLDivElement>} className={cn(\"flex flex-col gap-1\", className)}>\n      <Price amount={price} compareAt={compareAt} size=\"lg\" showSavings />\n      {eligible ? (\n        <p className=\"flex flex-wrap items-baseline gap-x-1.5 text-[13px] text-sk-muted tabular-nums\">\n          <span>\n            or {installments} interest-free payments of <span className=\"font-medium text-sk-ink\">{money(parts[0])}</span> with {provider}\n          </span>\n          <button\n            ref={triggerRef}\n            type=\"button\"\n            aria-haspopup=\"dialog\"\n            onClick={() => {\n              sk.emit(\"interact\", \"financing_details\", price)\n              setOpen(true)\n            }}\n            className={cn(\"relative inline-flex min-h-[32px] items-center rounded-sm font-medium text-sk-ink underline decoration-sk-line underline-offset-4 transition-[text-decoration-color] duration-200 after:absolute after:-inset-x-1 after:-inset-y-1.5 after:content-[''] hover:decoration-sk-ink\", RING)}\n          >\n            How it works\n          </button>\n        </p>\n      ) : null}\n\n      <AnimatePresence>\n        {open ? (\n          <div className=\"fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-6\" onKeyDown={onKeyDown}>\n            <motion.div\n              key=\"scrim\"\n              aria-hidden\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: reduce ? 0 : 0.2 }}\n              onClick={close}\n              className=\"absolute inset-0 bg-sk-ink/40\"\n            />\n            <motion.div\n              key=\"panel\"\n              ref={panelRef}\n              role=\"dialog\"\n              aria-modal=\"true\"\n              aria-labelledby={`${uid}-title`}\n              initial={{ opacity: 0, y: reduce ? 0 : 16 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: reduce ? 0 : 12 }}\n              transition={{ duration: reduce ? 0 : 0.35, ease: EASE }}\n              className=\"relative flex max-h-[90dvh] w-full max-w-md flex-col gap-5 overflow-y-auto overscroll-contain rounded-t-sk-card border border-sk-line bg-sk-card p-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] shadow-[0_24px_60px_-24px_rgb(var(--sk-ink)/0.25)] sm:rounded-sk-card sm:p-6\"\n            >\n              <div className=\"flex items-start justify-between gap-4\">\n                <div className=\"flex flex-col gap-1\">\n                  <span className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{provider}</span>\n                  <h2 id={`${uid}-title`} className=\"font-sk-display text-xl font-semibold leading-[1.1] tracking-[-0.02em] text-sk-ink tabular-nums\">\n                    {money(price)} in {installments} payments\n                  </h2>\n                </div>\n                <button\n                  ref={closeRef}\n                  type=\"button\"\n                  aria-label=\"Close\"\n                  onClick={close}\n                  className={cn(\"-mr-2 -mt-2 grid h-11 w-11 shrink-0 place-items-center rounded-sk-btn text-sk-muted transition-[color,background-color,transform] duration-200 hover:bg-sk-ink/[0.05] hover:text-sk-ink active:scale-[0.97]\", RING)}\n                >\n                  <X size={18} />\n                </button>\n              </div>\n\n              <ol className=\"grid grid-cols-2 gap-2 sm:grid-cols-4\">\n                {parts.map((amt, i) => (\n                  <motion.li\n                    key={i}\n                    initial={{ opacity: 0, y: reduce ? 0 : 8 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    transition={{ duration: reduce ? 0 : 0.4, ease: EASE, delay: reduce ? 0 : 0.08 + i * 0.05 }}\n                    className=\"flex flex-col gap-1 rounded-sk-btn bg-sk-surface p-3 tabular-nums\"\n                  >\n                    <span className=\"text-[12px] text-sk-muted\">{i === 0 ? \"Today\" : `Week ${i * intervalWeeks}`}</span>\n                    <span className=\"text-sm font-semibold text-sk-ink\">{money(amt)}</span>\n                  </motion.li>\n                ))}\n              </ol>\n\n              <ul className=\"flex flex-col gap-2.5 border-t border-sk-line pt-4\">\n                {terms.map((t) => (\n                  <li key={t} className=\"flex gap-2.5 text-sm leading-[1.5] text-sk-muted\">\n                    <span aria-hidden className=\"mt-[9px] h-1 w-1 shrink-0 rounded-full bg-sk-muted\" />\n                    {t}\n                  </li>\n                ))}\n              </ul>\n\n              <Button variant=\"outline\" className=\"w-full active:scale-[0.97]\" onClick={close}>\n                Got it\n              </Button>\n              <p className=\"text-[12px] leading-[1.5] text-sk-muted\">Choose {provider} at checkout. Payment options are offered by the provider, not by this store.</p>\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/upsell/upsell-financing.tsx"
    }
  ],
  "meta": {
    "section": "upsell",
    "variant": "financing",
    "niche": [
      "any"
    ],
    "commerce": [
      "financing"
    ],
    "previewHeight": 200
  }
}