{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pricing-toggle",
  "type": "registry:block",
  "title": "Pricing Toggle",
  "description": "Three pricing tiers under a monthly/yearly switch whose thumb stretches on a spring. Every price rolls over like an odometer, one digit column at a time, while the old price strikes through and a savings chip slides in.",
  "docs": "Pricing Toggle was added to public/vividsites/pricing-toggle.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/pricing-toggle",
  "categories": [
    "ui",
    "cards-layout"
  ],
  "files": [
    {
      "path": "components/vividsites/pricing-toggle.html",
      "type": "registry:file",
      "target": "public/vividsites/pricing-toggle.html",
      "content": "<!--\n  vs-pricing-toggle: three pricing tiers with a monthly / yearly switch and odometer price digits.\n\n  - The switch is a radiogroup (click, or arrow keys). Its ink thumb rides a spring and stretches\n    a little with its own speed, then settles round.\n  - Each price is split into digit columns that roll on their own spring, one after another, so\n    48 → 38 turns over like a counter (right to left when a price drops, left to right when it\n    rises). Proportional figures close up as they roll. Put both prices on the element:\n      <span class=\"vs-pricing-toggle-price\" data-monthly=\"48\" data-yearly=\"38\" data-currency=\"$\"></span>\n    Prices with different digit counts are handled (the leading column fades out). On yearly the\n    old monthly price appears struck through beside it, and each card's \"Save\" chip slides in.\n  - Options (attributes on .vs-pricing-toggle, live: change them any time):\n      data-period=\"monthly\" | \"yearly\"   billing shown\n      data-highlight=\"1\" | \"2\" | \"3\"     which card is featured (dark card, accent CTA, the badge moves there); \"0\" = none\n    It fires \"vs-pricing-toggle:change\" with { period } in detail, and el.vsPricing.setPeriod(p) is there too.\n  - Cards lift on hover with a soft accent glow that follows the pointer (transform-only).\n  - prefers-reduced-motion: digits and thumb change instantly, no transitions.\n\n  CSS variables (on .vs-pricing-toggle):\n    --vs-pricing-toggle-paper      card + switch background\n    --vs-pricing-toggle-ink        text colour, switch thumb, CTA hover fill\n    --vs-pricing-toggle-accent     save chips, checks, featured CTA, badge\n    --vs-pricing-toggle-featured   featured card background\n    --vs-pricing-toggle-radius     card radius (buttons follow at 55%)\n-->\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500&family=Space+Mono&display=swap\" rel=\"stylesheet\">\n<style>\n.vs-pricing-toggle{\n  --vs-pricing-toggle-paper: #ffffff;\n  --vs-pricing-toggle-ink: #0e1116;\n  --vs-pricing-toggle-accent: #3355ff;\n  --vs-pricing-toggle-featured: #0e1116;\n  --vs-pricing-toggle-radius: 20px;\n  --vs-pricing-toggle-ease: cubic-bezier(.16,1,.3,1);\n  color: var(--vs-pricing-toggle-ink); font-family: \"Inter Tight\", system-ui, sans-serif;\n  display: flex; flex-direction: column; align-items: center; gap: 28px; width: 100%;\n  -webkit-font-smoothing: antialiased;\n}\n\n/* ---- billing switch: ink thumb on a white well, thumb rides a spring and stretches with its speed ---- */\n.vs-pricing-toggle-switch{\n  position: relative; display: grid; grid-template-columns: 1fr 1fr; padding: 4px; border-radius: 999px;\n  background: var(--vs-pricing-toggle-paper);\n  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vs-pricing-toggle-ink) 9%, transparent),\n              0 1px 2px rgba(14,17,22,.05), 0 8px 24px -14px rgba(14,17,22,.22);\n}\n.vs-pricing-toggle-thumb{\n  position: absolute; top: 4px; bottom: 4px; left: 4px; width: calc(50% - 4px); border-radius: 999px;\n  background: var(--vs-pricing-toggle-ink); will-change: transform; transform-origin: 50% 50%;\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.14), 0 1px 2px rgba(14,17,22,.3), 0 6px 14px -6px rgba(14,17,22,.45);\n}\n.vs-pricing-toggle-opt{\n  position: relative; z-index: 1; appearance: none; border: 0; background: transparent; cursor: pointer; margin: 0;\n  padding: 11px 22px; min-width: 138px; border-radius: 999px; color: var(--vs-pricing-toggle-ink);\n  font: 500 14px/1 \"Inter Tight\", system-ui, sans-serif; letter-spacing: -.01em;\n  display: inline-flex; align-items: center; justify-content: center; gap: 8px;\n  transition: color .35s var(--vs-pricing-toggle-ease), transform .2s var(--vs-pricing-toggle-ease);\n  -webkit-tap-highlight-color: transparent;\n}\n.vs-pricing-toggle-opt:not([aria-checked=\"true\"]){ color: color-mix(in srgb, var(--vs-pricing-toggle-ink) 55%, transparent); }\n.vs-pricing-toggle-opt:not([aria-checked=\"true\"]):hover{ color: var(--vs-pricing-toggle-ink); }\n.vs-pricing-toggle-opt[aria-checked=\"true\"]{ color: var(--vs-pricing-toggle-paper); }\n.vs-pricing-toggle-opt:active{ transform: scale(.96); }\n.vs-pricing-toggle-opt:focus-visible{ outline: 2px solid var(--vs-pricing-toggle-accent); outline-offset: 2px; }\n.vs-pricing-toggle-save{\n  font: 400 10.5px/1 \"Space Mono\", ui-monospace, monospace; letter-spacing: 0; padding: 4px 6px 3px; border-radius: 999px;\n  color: var(--vs-pricing-toggle-accent); background: color-mix(in srgb, var(--vs-pricing-toggle-accent) 12%, transparent);\n  transition: color .35s var(--vs-pricing-toggle-ease), background-color .35s var(--vs-pricing-toggle-ease);\n}\n.vs-pricing-toggle[data-period=\"yearly\"] .vs-pricing-toggle-save{ color: #fff; background: var(--vs-pricing-toggle-accent); animation: vs-pricing-toggle-pop .6s cubic-bezier(.34,1.8,.5,1); }\n@keyframes vs-pricing-toggle-pop{ 0%{ transform: scale(.7) rotate(-6deg); } 100%{ transform: none; } }\n\n/* ---- cards ---- */\n.vs-pricing-toggle-grid{ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; width: 100%; align-items: stretch; }\n.vs-pricing-toggle-card{\n  --c-ink: var(--vs-pricing-toggle-ink); --c-bg: var(--vs-pricing-toggle-paper); --c-line: color-mix(in srgb, var(--vs-pricing-toggle-ink) 9%, transparent);\n  position: relative; overflow: hidden; isolation: isolate; border-radius: var(--vs-pricing-toggle-radius);\n  padding: 22px 22px 24px; display: flex; flex-direction: column; background: var(--c-bg); color: var(--c-ink);\n  box-shadow: inset 0 0 0 1px var(--c-line), inset 0 1px 0 rgba(255,255,255,.9),\n              0 1px 1px rgba(14,17,22,.03), 0 2px 4px rgba(14,17,22,.03), 0 24px 48px -30px rgba(14,17,22,.28);\n  transition: transform .6s var(--vs-pricing-toggle-ease), box-shadow .6s var(--vs-pricing-toggle-ease),\n              background-color .5s var(--vs-pricing-toggle-ease), color .5s var(--vs-pricing-toggle-ease), border-radius .3s;\n}\n.vs-pricing-toggle-card:hover{ transform: translate3d(0,-4px,0);\n  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vs-pricing-toggle-ink) 14%, transparent), inset 0 1px 0 rgba(255,255,255,.9),\n              0 1px 1px rgba(14,17,22,.03), 0 4px 8px rgba(14,17,22,.04), 0 34px 60px -32px rgba(14,17,22,.36); }\n.vs-pricing-toggle-card.is-featured{\n  --c-ink: #f4f5f7; --c-bg: var(--vs-pricing-toggle-featured); --c-line: rgba(255,255,255,.1);\n  box-shadow: inset 0 0 0 1px rgba(255,255,255,.08), inset 0 1px 0 rgba(255,255,255,.12),\n              0 2px 4px rgba(14,17,22,.12), 0 40px 70px -34px color-mix(in srgb, var(--vs-pricing-toggle-accent) 55%, rgba(14,17,22,.6));\n}\n/* pointer glow + a hairline rim that lights toward the pointer */\n.vs-pricing-toggle-glow{\n  position: absolute; left: 0; top: 0; width: 460px; height: 460px; margin: -230px 0 0 -230px; z-index: -1; pointer-events: none;\n  border-radius: 50%; opacity: 0; will-change: transform, opacity; transition: opacity .5s var(--vs-pricing-toggle-ease);\n  background: radial-gradient(circle, color-mix(in srgb, var(--vs-pricing-toggle-accent) 11%, transparent) 0%, transparent 60%);\n}\n.vs-pricing-toggle-card.is-featured .vs-pricing-toggle-glow{ background: radial-gradient(circle, color-mix(in srgb, var(--vs-pricing-toggle-accent) 24%, transparent) 0%, transparent 52%); }\n.vs-pricing-toggle-card:hover .vs-pricing-toggle-glow{ opacity: 1; }\n\n.vs-pricing-toggle-top{ display: flex; justify-content: space-between; align-items: center; gap: 10px; height: 24px; }\n.vs-pricing-toggle-name{ margin: 0; font: 500 15px/1 \"Inter Tight\", system-ui, sans-serif; letter-spacing: -.01em; }\n.vs-pricing-toggle-badge{\n  display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px 5px 8px; border-radius: 999px;\n  font: 400 10.5px/1 \"Space Mono\", ui-monospace, monospace; letter-spacing: .02em; text-transform: uppercase;\n  color: #fff; background: var(--vs-pricing-toggle-accent);\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 4px 12px -4px color-mix(in srgb, var(--vs-pricing-toggle-accent) 70%, transparent);\n  animation: vs-pricing-toggle-badge .7s cubic-bezier(.34,1.56,.64,1) both;\n}\n.vs-pricing-toggle-badge::before{ content: \"\"; width: 5px; height: 5px; border-radius: 50%; background: #fff; }\n@keyframes vs-pricing-toggle-badge{ from{ opacity: 0; transform: translate3d(0,-6px,0) scale(.8); } }\n.vs-pricing-toggle-for{ margin: 8px 0 0; font-size: 13.5px; line-height: 1.45; opacity: .58; min-height: 2.9em; text-wrap: pretty; }\n\n/* ---- the price: odometer columns ---- */\n.vs-pricing-toggle-amount{ display: flex; align-items: flex-end; gap: 10px; margin: 18px 0 0; }\n.vs-pricing-toggle-price{\n  display: inline-flex; align-items: flex-start; font: 500 60px/1 \"Inter Tight\", system-ui, sans-serif; letter-spacing: -.05em;\n  position: relative;\n}\n.vs-pricing-toggle-cur{ font-size: .4em; font-weight: 500; line-height: 1; margin: .14em .08em 0 0; opacity: .5; letter-spacing: 0; }\n.vs-pricing-toggle-digits{ position: relative; display: inline-block; height: 1em; }\n.vs-pricing-toggle-col{ position: absolute; left: 0; top: 0; height: 1em; overflow: hidden; will-change: transform;\n  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 84%, transparent 100%);\n          mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 84%, transparent 100%); }\n.vs-pricing-toggle-strip{ position: absolute; left: 0; top: 0; width: 100%; display: flex; flex-direction: column; will-change: transform; }\n.vs-pricing-toggle-strip span{ display: block; height: 1em; text-align: left; }\n.vs-pricing-toggle-per{ display: grid; gap: 4px; padding-bottom: 7px; will-change: transform;\n  font: 400 11px/1 \"Space Mono\", ui-monospace, monospace; letter-spacing: -.01em; }\n.vs-pricing-toggle-per > span{ opacity: .5; white-space: nowrap; }\n.vs-pricing-toggle-per > .vs-pricing-toggle-was{ opacity: 0; transform: translate3d(0,6px,0); text-decoration: line-through; text-decoration-thickness: 1px;\n  transition: opacity .35s var(--vs-pricing-toggle-ease), transform .6s var(--vs-pricing-toggle-ease); white-space: nowrap; }\n.vs-pricing-toggle[data-period=\"yearly\"] .vs-pricing-toggle-per > .vs-pricing-toggle-was{ opacity: .4; transform: none; transition-delay: .12s; }\n\n.vs-pricing-toggle-note{ position: relative; height: 22px; margin: 12px 0 0; font-size: 12.5px; }\n.vs-pricing-toggle-note > span{ position: absolute; left: 0; top: 0; height: 22px; display: inline-flex; align-items: center; gap: 8px; white-space: nowrap;\n  transition: opacity .35s var(--vs-pricing-toggle-ease), transform .6s var(--vs-pricing-toggle-ease); }\n.vs-pricing-toggle-note > span > i{ font-style: normal; opacity: .55; }\n.vs-pricing-toggle-off{ font: 400 10.5px/1 \"Space Mono\", ui-monospace, monospace; padding: 5px 7px 4px; border-radius: 6px;\n  color: var(--vs-pricing-toggle-accent); background: color-mix(in srgb, var(--vs-pricing-toggle-accent) 11%, transparent); }\n.vs-pricing-toggle-card.is-featured .vs-pricing-toggle-off{ color: #fff; background: color-mix(in srgb, var(--vs-pricing-toggle-accent) 70%, transparent); }\n.vs-pricing-toggle[data-period=\"monthly\"] .vs-pricing-toggle-note [data-for=\"yearly\"]{ opacity: 0; transform: translate3d(0,10px,0); pointer-events: none; transition-duration: .18s, .6s; }\n.vs-pricing-toggle[data-period=\"yearly\"] .vs-pricing-toggle-note [data-for=\"monthly\"]{ opacity: 0; transform: translate3d(0,-10px,0); pointer-events: none; transition-duration: .18s, .6s; }\n.vs-pricing-toggle-note > span{ transition-delay: .1s; }  /* incoming line waits for the outgoing one */\n.vs-pricing-toggle[data-period=\"monthly\"] .vs-pricing-toggle-note [data-for=\"yearly\"],\n.vs-pricing-toggle[data-period=\"yearly\"] .vs-pricing-toggle-note [data-for=\"monthly\"]{ transition-delay: 0s; }\n\n/* ---- CTA: ink fill slides up; featured card uses the accent ---- */\n.vs-pricing-toggle-cta{\n  position: relative; overflow: hidden; isolation: isolate; display: flex; align-items: center; justify-content: center; gap: 8px;\n  margin: 18px 0 0; padding: 13px 18px; border-radius: calc(var(--vs-pricing-toggle-radius) * .55); text-decoration: none; color: inherit;\n  font: 500 14px/1 \"Inter Tight\", system-ui, sans-serif; letter-spacing: -.01em;\n  box-shadow: inset 0 0 0 1px var(--c-line), 0 1px 2px rgba(14,17,22,.05);\n  background: color-mix(in srgb, var(--c-ink) 3%, transparent);\n  transition: color .35s var(--vs-pricing-toggle-ease), transform .18s var(--vs-pricing-toggle-ease), box-shadow .35s;\n  -webkit-tap-highlight-color: transparent;\n}\n.vs-pricing-toggle-cta::before{\n  content: \"\"; position: absolute; inset: 0; z-index: -1; background: var(--c-ink); border-radius: inherit;\n  transform: translate3d(0,102%,0); transition: transform .55s var(--vs-pricing-toggle-ease);\n}\n.vs-pricing-toggle-cta:hover{ color: var(--c-bg); }\n.vs-pricing-toggle-cta:hover::before{ transform: none; }\n.vs-pricing-toggle-cta:active{ transform: scale(.975); }\n.vs-pricing-toggle-cta:focus-visible{ outline: 2px solid var(--vs-pricing-toggle-accent); outline-offset: 3px; }\n.vs-pricing-toggle-cta svg{ width: 14px; height: 14px; transition: transform .55s var(--vs-pricing-toggle-ease); }\n.vs-pricing-toggle-cta:hover svg{ transform: translate3d(3px,0,0); }\n.vs-pricing-toggle-card.is-featured .vs-pricing-toggle-cta{ color: #fff; background: var(--vs-pricing-toggle-accent);\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.22), inset 0 -1px 0 rgba(0,0,0,.12), 0 8px 20px -8px color-mix(in srgb, var(--vs-pricing-toggle-accent) 80%, transparent); }\n.vs-pricing-toggle-card.is-featured .vs-pricing-toggle-cta::before{ background: #fff; }\n.vs-pricing-toggle-card.is-featured .vs-pricing-toggle-cta:hover{ color: var(--vs-pricing-toggle-featured); }\n.vs-pricing-toggle-cta[aria-disabled=\"true\"]{ pointer-events: none; opacity: .45; }\n\n.vs-pricing-toggle-list{ list-style: none; margin: 20px 0 0; padding: 18px 0 0; display: grid; gap: 10px;\n  border-top: 1px dashed var(--c-line); font-size: 13.5px; line-height: 1.35; }\n.vs-pricing-toggle-list li{ display: grid; grid-template-columns: 16px 1fr; gap: 10px; align-items: start; }\n.vs-pricing-toggle-list li span{ opacity: .8; }\n.vs-pricing-toggle-list svg{ width: 16px; height: 16px; margin-top: 0; color: var(--vs-pricing-toggle-accent); }\n.vs-pricing-toggle-card.is-featured .vs-pricing-toggle-list svg{ color: color-mix(in srgb, var(--vs-pricing-toggle-accent) 55%, #fff); }\n.vs-pricing-toggle-list li.is-lead span{ opacity: .5; }\n\n.vs-pricing-toggle-sr{ position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n\n@media (max-width: 860px){\n  .vs-pricing-toggle-grid{ grid-template-columns: 1fr; max-width: 420px; }\n  .vs-pricing-toggle-for{ min-height: 0; }\n}\n@media (prefers-reduced-motion: reduce){\n  .vs-pricing-toggle *, .vs-pricing-toggle *::before{ transition: none !important; animation: none !important; }\n}\n</style>\n\n<div class=\"vs-pricing-toggle\" data-period=\"monthly\" data-highlight=\"2\">\n  <div class=\"vs-pricing-toggle-switch\" role=\"radiogroup\" aria-label=\"Billing period\">\n    <span class=\"vs-pricing-toggle-thumb\" aria-hidden=\"true\"></span>\n    <button class=\"vs-pricing-toggle-opt\" type=\"button\" role=\"radio\" data-period=\"monthly\">Monthly</button>\n    <button class=\"vs-pricing-toggle-opt\" type=\"button\" role=\"radio\" data-period=\"yearly\">Yearly <span class=\"vs-pricing-toggle-save\">&minus;20%</span></button>\n  </div>\n  <div class=\"vs-pricing-toggle-grid\">\n    <article class=\"vs-pricing-toggle-card\">\n      <span class=\"vs-pricing-toggle-glow\" aria-hidden=\"true\"></span>\n      <div class=\"vs-pricing-toggle-top\"><h3 class=\"vs-pricing-toggle-name\">Solo</h3></div>\n      <p class=\"vs-pricing-toggle-for\">For one curious person and a stack of notebooks.</p>\n      <div class=\"vs-pricing-toggle-amount\"><span class=\"vs-pricing-toggle-price\" data-currency=\"$\" data-monthly=\"18\" data-yearly=\"14\">$18</span><span class=\"vs-pricing-toggle-per\"><span>per seat / mo</span></span></div>\n      <p class=\"vs-pricing-toggle-note\"><span data-for=\"monthly\"><i>Billed monthly, cancel any time</i></span><span data-for=\"yearly\"><b class=\"vs-pricing-toggle-off\">Save $48</b><i>$168 billed yearly</i></span></p>\n      <a class=\"vs-pricing-toggle-cta\" href=\"#\">Start with Solo<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 8h10\"/><path d=\"M9 4l4 4-4 4\"/></svg></a>\n      <ul class=\"vs-pricing-toggle-list\"><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Unlimited notes and sketches</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>5 GB of scans and audio</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Offline on two devices</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Markdown and PDF export</span></li></ul>\n    </article>\n    <article class=\"vs-pricing-toggle-card\">\n      <span class=\"vs-pricing-toggle-glow\" aria-hidden=\"true\"></span>\n      <div class=\"vs-pricing-toggle-top\"><h3 class=\"vs-pricing-toggle-name\">Studio</h3> <span class=\"vs-pricing-toggle-badge\">Most chosen</span></div>\n      <p class=\"vs-pricing-toggle-for\">For a small team that shares a desk and a deadline.</p>\n      <div class=\"vs-pricing-toggle-amount\"><span class=\"vs-pricing-toggle-price\" data-currency=\"$\" data-monthly=\"48\" data-yearly=\"38\">$48</span><span class=\"vs-pricing-toggle-per\"><span>per seat / mo</span></span></div>\n      <p class=\"vs-pricing-toggle-note\"><span data-for=\"monthly\"><i>Billed monthly, cancel any time</i></span><span data-for=\"yearly\"><b class=\"vs-pricing-toggle-off\">Save $120</b><i>$456 billed yearly</i></span></p>\n      <a class=\"vs-pricing-toggle-cta\" href=\"#\">Start a 14-day trial<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 8h10\"/><path d=\"M9 4l4 4-4 4\"/></svg></a>\n      <ul class=\"vs-pricing-toggle-list\"><li class=\"is-lead\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Everything in Solo</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Shared notebooks for ten</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Comments and review rounds</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Version history, 90 days</span></li></ul>\n    </article>\n    <article class=\"vs-pricing-toggle-card\">\n      <span class=\"vs-pricing-toggle-glow\" aria-hidden=\"true\"></span>\n      <div class=\"vs-pricing-toggle-top\"><h3 class=\"vs-pricing-toggle-name\">House</h3></div>\n      <p class=\"vs-pricing-toggle-for\">For a whole floor, with the paperwork your IT lead wants.</p>\n      <div class=\"vs-pricing-toggle-amount\"><span class=\"vs-pricing-toggle-price\" data-currency=\"$\" data-monthly=\"128\" data-yearly=\"102\">$128</span><span class=\"vs-pricing-toggle-per\"><span>per seat / mo</span></span></div>\n      <p class=\"vs-pricing-toggle-note\"><span data-for=\"monthly\"><i>Billed monthly, cancel any time</i></span><span data-for=\"yearly\"><b class=\"vs-pricing-toggle-off\">Save $312</b><i>$1,224 billed yearly</i></span></p>\n      <a class=\"vs-pricing-toggle-cta\" href=\"#\">Talk to us<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 8h10\"/><path d=\"M9 4l4 4-4 4\"/></svg></a>\n      <ul class=\"vs-pricing-toggle-list\"><li class=\"is-lead\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Everything in Studio</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>Unlimited members</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>SSO and an audit log</span></li><li><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg><span>A named person for support</span></li></ul>\n    </article>\n  </div>\n</div>\n\n<script>\n(() => {\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const CELLS = 11;   // 0–9 plus a blank for a leading column the other price doesn't need\n  function init(root) {\n    if (root.dataset.vsReady) return; root.dataset.vsReady = '1';\n    let period = root.dataset.period === 'yearly' ? 'yearly' : 'monthly';\n    root.dataset.period = period;\n    const opts = [...root.querySelectorAll('.vs-pricing-toggle-opt')];\n    const thumb = root.querySelector('.vs-pricing-toggle-thumb');\n    const cards = [...root.querySelectorAll('.vs-pricing-toggle-card')];\n    const badge = root.querySelector('.vs-pricing-toggle-badge');\n    const springs = [];          // every moving thing is a spring: { x, v, t, k, c, apply, delay }\n    let raf = 0, last = 0;\n\n    // the thumb: position spring; it stretches a touch along its travel with its own speed\n    const thumbS = { x: period === 'yearly' ? 1 : 0, v: 0, k: 420, c: 32, apply: s => {\n      if (!thumb) return;\n      const st = Math.min(0.14, Math.abs(s.v) * 0.028);\n      thumb.style.transform = `translate3d(${(s.x * 100).toFixed(3)}%,0,0) scale(${(1 + st).toFixed(4)},${(1 - st * 0.35).toFixed(4)})`;\n    } };\n    thumbS.t = thumbS.x; springs.push(thumbS);\n\n    // proportional odometer: every column sits at an x that is itself interpolated from the rolling digits,\n    // so narrow figures (a \"1\") close up while they turn over. Transform-only.\n    const ctx = document.createElement('canvas').getContext('2d');\n    const prices = [...root.querySelectorAll('.vs-pricing-toggle-price')].map(el => {\n      const cur$ = el.dataset.currency ?? '$';\n      const vals = { monthly: String(el.dataset.monthly ?? ''), yearly: String(el.dataset.yearly ?? '') };\n      const width = Math.max(vals.monthly.length, vals.yearly.length);\n      const cur = document.createElement('span'); cur.className = 'vs-pricing-toggle-cur'; cur.textContent = cur$; cur.setAttribute('aria-hidden', 'true');\n      const digits = document.createElement('span'); digits.className = 'vs-pricing-toggle-digits'; digits.setAttribute('aria-hidden', 'true');\n      const sr = document.createElement('span'); sr.className = 'vs-pricing-toggle-sr';\n      el.textContent = ''; el.append(cur, digits, sr);\n      const per = el.nextElementSibling && el.nextElementSibling.classList.contains('vs-pricing-toggle-per') ? el.nextElementSibling : null;\n      if (per && vals.monthly !== vals.yearly && !per.querySelector('.vs-pricing-toggle-was')) {   // struck-through monthly price, shown on yearly\n        const was = document.createElement('span'); was.className = 'vs-pricing-toggle-was'; was.setAttribute('aria-hidden', 'true');\n        was.textContent = cur$ + vals.monthly; per.prepend(was);\n      }\n      const idx = (str, j) => { const ch = str.padStart(width, ' ')[j]; return ch === ' ' ? 10 : +ch; };\n      let W = new Array(CELLS).fill(0), wMax = 0, total = 0;\n      const cols = [];\n      const place = () => {\n        let x = 0;\n        cols.forEach(c => {\n          const v = Math.max(0, Math.min(10, c.s.x)), lo = Math.floor(v), hi = Math.min(10, lo + 1), f = v - lo;\n          c.col.style.transform = `translate3d(${x.toFixed(2)}px,0,0)`;\n          c.col.style.opacity = String(Math.max(0, Math.min(1, 10 - v)));\n          x += W[lo] + (W[hi] - W[lo]) * f;\n        });\n        if (per) per.style.transform = `translate3d(${(x - total).toFixed(2)}px,0,0)`;\n      };\n      const measure = () => {\n        const cs = getComputedStyle(digits);\n        ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;\n        const ls = parseFloat(cs.letterSpacing) || 0;\n        W = [...'0123456789'].map(d => ctx.measureText(d).width + ls).concat(0);\n        wMax = Math.max(...W);\n        const tot = str => [...str.padStart(width, ' ')].reduce((a, ch) => a + (ch === ' ' ? 0 : W[+ch]), 0);\n        total = Math.max(tot(vals.monthly), tot(vals.yearly));\n        digits.style.width = total.toFixed(2) + 'px';\n        cols.forEach(c => c.col.style.width = (wMax + 2).toFixed(2) + 'px');\n        place();\n      };\n      for (let j = 0; j < width; j++) {\n        const col = document.createElement('span'); col.className = 'vs-pricing-toggle-col';\n        const strip = document.createElement('span'); strip.className = 'vs-pricing-toggle-strip';\n        for (let d = 0; d < CELLS; d++) { const c = document.createElement('span'); c.textContent = d < 10 ? d : ' '; strip.append(c); }\n        col.append(strip); digits.append(col);\n        const s = { x: idx(vals[period], j), v: 0, k: 190, c: 20, apply: s => { strip.style.transform = `translate3d(0,${(-s.x / CELLS * 100).toFixed(3)}%,0)`; place(); } };\n        s.t = s.x; springs.push(s); cols.push({ s, col });\n      }\n      measure();\n      if (document.fonts) document.fonts.ready.then(measure);\n      addEventListener('resize', measure);\n      const label = p => { sr.textContent = `${cur$}${vals[p]} per seat per month, billed ${p}`; };\n      label(period);\n      // columns start right to left when a price drops, left to right when it rises — like a real counter carrying\n      return { set: p => { const up = +vals[p] > +vals[p === 'yearly' ? 'monthly' : 'yearly'];\n        cols.forEach((c, j) => { c.s.t = idx(vals[p], j); c.s.delay = (up ? j : cols.length - 1 - j) * 60; }); label(p); } };\n    });\n    springs.forEach(s => s.apply(s));\n\n    const tick = now => {\n      const dtMs = Math.min(50, now - last); last = now;\n      const n = Math.max(1, Math.ceil(dtMs / 1000 * 240)), h = dtMs / 1000 / n;\n      let moving = false;\n      for (const s of springs) {\n        if (s.delay > 0) { s.delay -= dtMs; moving = true; continue; }\n        if (Math.abs(s.x - s.t) < 1e-4 && Math.abs(s.v) < 1e-3) { if (s.x !== s.t || s.v) { s.x = s.t; s.v = 0; s.apply(s); } continue; }\n        for (let i = 0; i < n; i++) { s.v += (-s.k * (s.x - s.t) - s.c * s.v) * h; s.x += s.v * h; }\n        s.apply(s); moving = true;\n      }\n      raf = moving ? requestAnimationFrame(tick) : 0;\n    };\n    const syncOpts = () => opts.forEach(o => { const on = o.dataset.period === period; o.setAttribute('aria-checked', String(on)); o.tabIndex = on ? 0 : -1; });\n    const setPeriod = p => {\n      p = p === 'yearly' ? 'yearly' : 'monthly';\n      if (p === period) return; period = p; if (root.dataset.period !== p) root.dataset.period = p;\n      syncOpts();\n      thumbS.t = p === 'yearly' ? 1 : 0;\n      prices.forEach(pr => pr.set(p));\n      root.dispatchEvent(new CustomEvent('vs-pricing-toggle:change', { detail: { period: p } }));\n      if (reduce.matches) { springs.forEach(s => { s.x = s.t; s.v = 0; s.delay = 0; s.apply(s); }); return; }\n      if (!raf) { last = performance.now(); raf = requestAnimationFrame(tick); }\n    };\n    // which card is featured: data-highlight=\"1|2|3\" (1-based; 0 = none). The badge travels with it.\n    const setHighlight = () => {\n      const n = parseInt(root.dataset.highlight, 10);\n      cards.forEach((c, i) => c.classList.toggle('is-featured', i + 1 === n));\n      const f = cards[n - 1];\n      if (badge) { if (f) { const top = f.querySelector('.vs-pricing-toggle-top'); if (badge.parentNode !== top) { top.append(badge); badge.style.animation = 'none'; badge.offsetWidth; badge.style.animation = ''; } badge.hidden = false; } else badge.hidden = true; }\n    };\n    syncOpts(); setHighlight();\n    opts.forEach(o => {\n      o.addEventListener('click', () => setPeriod(o.dataset.period));\n      o.addEventListener('keydown', e => {\n        if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return;\n        e.preventDefault(); const other = opts.find(x => x !== o); other.focus(); setPeriod(other.dataset.period);\n      });\n    });\n    // attributes are the API: set data-period / data-highlight from anywhere and the component follows\n    new MutationObserver(() => { if (root.dataset.period !== period) setPeriod(root.dataset.period); setHighlight(); })\n      .observe(root, { attributes: true, attributeFilter: ['data-period', 'data-highlight'] });\n    root.vsPricing = { setPeriod };\n\n    // pointer glow: transform-only, one rAF per move\n    cards.forEach(card => {\n      const glow = card.querySelector('.vs-pricing-toggle-glow'); if (!glow) return;\n      let pend = null;\n      card.addEventListener('pointermove', e => {\n        if (e.pointerType !== 'mouse') return;\n        const r = card.getBoundingClientRect(); const x = e.clientX - r.left, y = e.clientY - r.top;\n        if (!pend) pend = requestAnimationFrame(() => { pend = null; glow.style.transform = `translate3d(${x}px,${y}px,0)`; });\n      });\n    });\n  }\n  const boot = () => document.querySelectorAll('.vs-pricing-toggle').forEach(init);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', boot) : boot();\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Cards & layout",
    "tags": [
      "pricing",
      "toggle",
      "odometer",
      "numbers",
      "saas",
      "cards",
      "switch",
      "spring"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [
      "https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500&family=Space+Mono&display=swap"
    ],
    "source": null,
    "free": true,
    "accent": "#3355ff",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/pricing-toggle/index.html",
    "thumbnail": "https://vividsites.app/api/component/pricing-toggle/thumb.jpg",
    "video": "https://vividsites.app/api/component/pricing-toggle/thumb.mp4",
    "page": "https://vividsites.app/c/pricing-toggle"
  }
}