{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"saas-pricing","type":"registry:block","title":"SaaS pricing","description":"\"pricing = 117\" — tiered table with billing toggle and rolling prices. The SaaS conversion workhorse.","author":"Motion Menu","categories":["role-pricing","domain","saas"],"dependencies":["gsap"],"registryDependencies":[],"meta":{"number":"117","slug":"saas-pricing","framework":"react","docs":"https://motion-menu-two.vercel.app/p/saas-pricing.md"},"files":[{"path":"motion-menu/saas-pricing.html","type":"registry:file","target":"motion-menu/saas-pricing.html","content":"<article class=\"card\">\n        <header><span class=\"num\">117</span><h3>SaaS pricing table</h3><div class=\"tags\"><i>SaaS</i></div><a class=\"format-action\" data-format-link data-pattern-slug=\"saas-pricing\" href=\"/r/saas-pricing.json?framework=react\" title=\"Open the React registry output\">React ↗</a></header>\n        <div class=\"demo\" style=\"flex-direction:column; padding:1.4rem 0;\">\n          <div class=\"bill-toggle\" id=\"bill-toggle\">monthly <button class=\"sw\" aria-label=\"toggle billing\"><i></i></button> annual <span class=\"save\">save 20%</span></div>\n          <div class=\"pricing\" id=\"pricing\"></div>\n        </div>\n        <p class=\"order\">Say: <b>\"pricing = 117\"</b> — tiered table with billing toggle and rolling prices. The SaaS conversion workhorse.</p>\n      </article>"},{"path":"motion-menu/saas-pricing.css","type":"registry:file","target":"motion-menu/saas-pricing.css","content":".card {\n    border: 1px solid var(--line); border-radius: 14px; background: var(--card);\n    overflow: hidden; display: flex; flex-direction: column;\n  }\n\n.card > header { display: flex; align-items: baseline; gap: .8rem; padding: 1.1rem 1.2rem .9rem; border-bottom: 1px solid var(--line); }\n\n.card .num { font-family: var(--mono); color: var(--acc); font-size: 1.05rem; }\n\n.card h3 { font-size: 1.02rem; font-weight: 600; letter-spacing: -0.01em; flex: 1; }\n\n.tags { display: flex; gap: .35rem; }\n\n.tags i {\n    font: 400 .62rem/1 var(--mono); font-style: normal; color: var(--dim);\n    border: 1px solid var(--line); border-radius: 99px; padding: .28em .6em; white-space: nowrap;\n  }\n\n.demo {\n    min-height: 250px; position: relative; display: flex; align-items: center; justify-content: center;\n    background: var(--bg2); overflow: hidden; flex: 1;\n  }\n\n.order { padding: .85rem 1.2rem; font-family: var(--mono); font-size: .72rem; color: var(--dim); border-top: 1px solid var(--line); }\n\n.order b { color: var(--txt); font-weight: 500; }\n\n.demo .btn.corner { position: absolute; top: 12px; right: 12px; padding: .55em 1em; font-size: .68rem; z-index: 5; }\n\n.ai-card .num { color: var(--acc2) !important; }\n\n/* ---------- Section Q · domain packs ---------- */\n  .pricing { display: flex; gap: 12px; width: 90%; align-items: stretch; }\n\n.tier { flex: 1; border: 1px solid var(--line); border-radius: 12px; padding: 1.1rem 1rem; position: relative; }\n\n.tier.hot { border-color: rgba(183,255,46,.5); }\n\n.tier .t-name { font: 500 .68rem var(--mono); color: var(--dim); text-transform: uppercase; letter-spacing: .15em; }\n\n.tier .t-price { font: 700 1.8rem var(--sans); margin: .5rem 0 .2rem; font-variant-numeric: tabular-nums; }\n\n.tier .t-price small { font-size: .8rem; color: var(--dim); font-weight: 400; }\n\n.tier .t-feat { font: 400 .64rem var(--mono); color: var(--dim); line-height: 1.9; }\n\n.tier .t-pop { position: absolute; top: -9px; right: 12px; font: 500 .55rem var(--mono); background: var(--acc); color: #0a0a0e;\n    border-radius: 99px; padding: .3em .8em; }\n\n.bill-toggle { display: flex; align-items: center; gap: .6rem; font: 400 .68rem var(--mono); color: var(--dim); margin-bottom: 1.1rem; }\n\n.bill-toggle .sw { width: 40px; height: 22px; border-radius: 99px; background: rgba(255,255,255,.12); border: 0; position: relative; }\n\n.bill-toggle .sw i { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--acc);\n    transition: transform .35s cubic-bezier(.76,0,.24,1); }\n\n.bill-toggle.annual .sw i { transform: translateX(18px); }\n\n.bill-toggle .save { color: var(--acc); }\n\n.demo:has(.hascheck input:checked) .hasbtn { border-color: var(--acc); color: var(--acc); pointer-events: auto; opacity: 1; }"},{"path":"components/motion-menu/saas-pricing.tsx","type":"registry:component","content":"// @ts-nocheck — generated wrapper; inlined pattern JS is imperative DOM code, not authored TS.\n\"use client\";\n// SaaS pricing — Motion Menu\nimport { useEffect, useRef } from \"react\";\n\nconst MARKUP = \"<article class=\\\"card\\\">\\n        <header><span class=\\\"num\\\">117</span><h3>SaaS pricing table</h3><div class=\\\"tags\\\"><i>SaaS</i></div><a class=\\\"format-action\\\" data-format-link data-pattern-slug=\\\"saas-pricing\\\" href=\\\"/r/saas-pricing.json?framework=react\\\" title=\\\"Open the React registry output\\\">React \\u2197</a></header>\\n        <div class=\\\"demo\\\" style=\\\"flex-direction:column; padding:1.4rem 0;\\\">\\n          <div class=\\\"bill-toggle\\\" id=\\\"bill-toggle\\\">monthly <button class=\\\"sw\\\" aria-label=\\\"toggle billing\\\"><i></i></button> annual <span class=\\\"save\\\">save 20%</span></div>\\n          <div class=\\\"pricing\\\" id=\\\"pricing\\\"></div>\\n        </div>\\n        <p class=\\\"order\\\">Say: <b>\\\"pricing = 117\\\"</b> \\u2014 tiered table with billing toggle and rolling prices. The SaaS conversion workhorse.</p>\\n      </article>\";\n\nexport function SaasPricing() {\n  const hostRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const host = hostRef.current;\n    if (!host) return;\n    const $ = (s: string) => host.querySelector(s) as HTMLElement | null;\n    let disposed = false;\n    const cleanup: Array<() => void> = [];\n\n    (async () => {\n      if (disposed) return;\n      try {\n        /* ---- 117 · SaaS pricing ---- */\n        {\n          const TIERS = [\n            { name: 'Starter', m: 29, feats: ['1 storefront', 'core patterns', 'email support'] },\n            { name: 'Studio', m: 79, hot: true, feats: ['3 storefronts', 'AI concierge', 'priority renders'] },\n            { name: 'House', m: 199, feats: ['unlimited', 'agent back-office', 'white glove'] },\n          ];\n          const wrap = document.getElementById('pricing');\n          const toggle = document.getElementById('bill-toggle');\n          const priceEls = [];\n          TIERS.forEach((t) => {\n            const d = document.createElement('div');\n            d.className = 'tier' + (t.hot ? ' hot' : '');\n            d.innerHTML = `${t.hot ? '<span class=\"t-pop\">popular</span>' : ''}\n              <div class=\"t-name\">${t.name}</div>\n              <div class=\"t-price\">$<span>${t.m}</span><small>/mo</small></div>\n              <div class=\"t-feat\">${t.feats.join('<br>')}</div>`;\n            wrap.appendChild(d);\n            priceEls.push(d.querySelector('.t-price span'));\n          });\n          let annual = false;\n          toggle.querySelector('.sw').addEventListener('click', () => {\n            annual = !annual;\n            toggle.classList.toggle('annual', annual);\n            TIERS.forEach((t, i) => {\n              const target = annual ? Math.round(t.m * 0.8) : t.m;\n              const obj = { v: Number(priceEls[i].textContent) };\n              gsap.to(obj, { v: target, duration: 0.6, ease: 'power2.out', onUpdate: () => (priceEls[i].textContent = Math.round(obj.v)) });\n            });\n          });\n        }\n      } catch (err: any) {\n        console.warn(\"[motion-menu] saas-pricing failed to start:\", err);\n      }\n    })();\n\n    return () => {\n      disposed = true;\n      cleanup.forEach((fn) => { try { fn(); } catch (_) {} });\n    };\n  }, []);\n\n  return <div ref={hostRef} dangerouslySetInnerHTML={{ __html: MARKUP }} />;\n}\n\nexport default SaasPricing;\n"}]}