{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feature-tabs",
  "type": "registry:block",
  "title": "Feature Tabs",
  "description": "A product-tour block whose tabs advance on their own with a progress bar that holds while you hover or focus it. Each swap lifts the old panel away and brings in the new one with its contents staggering in, over four live product-UI mocks; arrow keys and touch swipes work too.",
  "docs": "Feature Tabs was added to public/vividsites/feature-tabs.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/feature-tabs",
  "categories": [
    "ui",
    "marketing-blocks"
  ],
  "files": [
    {
      "path": "components/vividsites/feature-tabs.html",
      "type": "registry:file",
      "target": "public/vividsites/feature-tabs.html",
      "content": "<!--\n  Feature Tabs — vividsites.app\n  A product-tour block: a list of features on one side, a media panel on the other. The tabs\n  advance on their own with a progress bar that fills under the active one; hover or focus the\n  block and it holds (the bar dims), leave and it carries on. Each swap lifts the old panel away\n  and brings the new one up with its contents arriving in a stagger, so the picture feels live.\n  Arrow keys, Home and End move between tabs; swipe the media on touch.\n\n  Markup: .vs-feature-tabs > .vs-feature-tabs-list[role=tablist] > button.vs-feature-tabs-tab (title + desc + bar)\n                           + .vs-feature-tabs-stage > .vs-feature-tabs-panel[role=tabpanel] (anything; children with [data-in] stagger in)\n  Options (data attributes): data-interval=\"6000\" (ms per tab), data-autoplay=\"false\" to turn auto-advance off\n  JS: el.vsFeatureTabs.select(i)   Event: \"vs-feature-tabs:change\" { index }\n  CSS variables:\n    --vs-feature-tabs-accent   progress bar, chart line and fills\n    --vs-feature-tabs-accent-ink  the accent used as text (defaults to the accent deepened for contrast;\n                               set it to the accent itself on a dark background)\n    --vs-feature-tabs-ink      titles\n    --vs-feature-tabs-muted    descriptions, inactive titles\n    --vs-feature-tabs-line     hairlines and bar track\n    --vs-feature-tabs-media    media panel fill\n    --vs-feature-tabs-radius   media radius\n  Reduced motion: no auto-advance; panels cross-fade.\n-->\n<style>\n.vs-feature-tabs {\n  --vs-feature-tabs-accent: #d99a1e;\n  --vs-feature-tabs-accent-ink: color-mix(in srgb, var(--vs-feature-tabs-accent) 62%, #000);  /* accent as text: deepened so it reads on light fills */\n  --vs-feature-tabs-ink: #14171c;\n  --vs-feature-tabs-muted: rgba(20,23,28,.5);\n  --vs-feature-tabs-line: rgba(20,23,28,.1);\n  --vs-feature-tabs-media: #ffffff;\n  --vs-feature-tabs-radius: 22px;\n  --vs-feature-tabs-font: inherit;\n\n  display: grid; grid-template-columns: minmax(0, .82fr) minmax(0, 1.18fr); gap: clamp(1.5rem, 4vw, 3.5rem); align-items: center;\n  width: 100%; font-family: var(--vs-feature-tabs-font); color: var(--vs-feature-tabs-ink);\n}\n.vs-feature-tabs, .vs-feature-tabs * { box-sizing: border-box; }\n.vs-feature-tabs-list { display: grid; }\n.vs-feature-tabs-tab {\n  position: relative; display: grid; grid-template-rows: auto 0fr auto; text-align: left; width: 100%;\n  padding: 1.15rem 0 1.15rem 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; outline: none;\n  border-top: 1px solid var(--vs-feature-tabs-line); -webkit-tap-highlight-color: transparent;\n  transition: grid-template-rows .55s cubic-bezier(.2,.8,.2,1);\n}\n.vs-feature-tabs-tab:last-child { border-bottom: 1px solid var(--vs-feature-tabs-line); }\n.vs-feature-tabs-tab[aria-selected=\"true\"] { grid-template-rows: auto 1fr auto; }\n.vs-feature-tabs-title { display: flex; align-items: baseline; gap: .9rem; font-size: 1.25rem; font-weight: 500; letter-spacing: -.015em; line-height: 1.25;\n  color: var(--vs-feature-tabs-muted); transition: color .35s cubic-bezier(.2,0,0,1), transform .5s cubic-bezier(.3,1.4,.5,1); }\n.vs-feature-tabs-title small { font: 500 .6875rem/1 ui-monospace, \"SF Mono\", Menlo, monospace; letter-spacing: .06em; font-variant-numeric: tabular-nums; opacity: .8; }\n.vs-feature-tabs-tab:hover .vs-feature-tabs-title { color: var(--vs-feature-tabs-ink); }\n.vs-feature-tabs-tab[aria-selected=\"true\"] .vs-feature-tabs-title { color: var(--vs-feature-tabs-ink); }\n.vs-feature-tabs-tab[aria-selected=\"true\"] .vs-feature-tabs-title small { color: var(--vs-feature-tabs-accent-ink); opacity: 1; }\n.vs-feature-tabs-tab:active .vs-feature-tabs-title { transform: translateX(2px); }\n.vs-feature-tabs-tab:focus-visible { box-shadow: inset 3px 0 0 var(--vs-feature-tabs-accent); }\n.vs-feature-tabs-tab:focus-visible .vs-feature-tabs-title { transform: translateX(10px); }\n.vs-feature-tabs-desc { overflow: hidden; min-height: 0; }\n.vs-feature-tabs-desc > span { display: block; padding: .55rem 0 0 2.05rem; max-width: 38ch; font-size: .9375rem; line-height: 1.55; color: var(--vs-feature-tabs-muted);\n  opacity: 0; transform: translateY(-4px); transition: opacity .3s cubic-bezier(.2,0,0,1), transform .5s cubic-bezier(.2,.8,.2,1); }\n.vs-feature-tabs-tab[aria-selected=\"true\"] .vs-feature-tabs-desc > span { opacity: 1; transform: none; transition-delay: .12s; }\n.vs-feature-tabs-bar { position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; z-index: 1; overflow: hidden; pointer-events: none; }\n.vs-feature-tabs-bar i { display: block; height: 100%; background: var(--vs-feature-tabs-accent); transform-origin: 0 50%; transform: scaleX(0); transition: opacity .3s; }\n.vs-feature-tabs.is-held .vs-feature-tabs-bar i { opacity: .35; }\n/* media stage */\n.vs-feature-tabs-stage {\n  position: relative; display: grid; aspect-ratio: 16 / 11.5; border-radius: var(--vs-feature-tabs-radius); overflow: hidden; touch-action: pan-y;\n  background: var(--vs-feature-tabs-media); isolation: isolate;\n  box-shadow: 0 0 0 1px var(--vs-feature-tabs-line), 0 1px 2px rgba(0,0,0,.04), 0 18px 40px -18px rgba(20,30,50,.28), 0 50px 90px -50px rgba(20,30,50,.35);\n}\n.vs-feature-tabs-panel { grid-area: 1 / 1; min-width: 0; min-height: 0; visibility: hidden; will-change: transform, opacity; }\n.vs-feature-tabs-panel.is-on { visibility: visible; }\n\n/* ── the four demo media (fictional product UI) ── */\n.vs-feature-tabs-m { position: absolute; inset: 0; display: grid; grid-template-rows: auto 1fr; font-size: clamp(.55rem, 1.05vw, .8125rem); background: var(--vs-feature-tabs-media); }\n.vs-feature-tabs-m header { display: flex; align-items: center; gap: .75em; padding: 1.1em 1.4em; border-bottom: 1px solid var(--vs-feature-tabs-line); font-weight: 600; }\n.vs-feature-tabs-m header em { font-style: normal; font-weight: 500; color: var(--vs-feature-tabs-muted); }\n.vs-feature-tabs-m header .sp { flex: 1; }\n.vs-feature-tabs-av { display: inline-grid; place-items: center; width: 2.1em; height: 2.1em; border-radius: 50%; font-size: .85em; font-weight: 700; color: #fff; box-shadow: 0 0 0 2px var(--vs-feature-tabs-media); }\n.vs-feature-tabs-av + .vs-feature-tabs-av { margin-left: -.6em; }\n.vs-feature-tabs-chip { display: inline-flex; align-items: center; gap: .4em; padding: .35em .7em; border-radius: 999px; font-weight: 600; font-size: .9em; background: color-mix(in srgb, var(--vs-feature-tabs-accent) 14%, transparent); color: var(--vs-feature-tabs-accent-ink); }\n/* 1: week planner */\n.vs-feature-tabs-week { display: grid; grid-template-columns: repeat(5, 1fr); gap: .7em; padding: 1.2em 1.4em; position: relative; }\n.vs-feature-tabs-day { display: grid; align-content: start; gap: .55em; }\n.vs-feature-tabs-day > b { font-size: .85em; font-weight: 600; color: var(--vs-feature-tabs-muted); padding-bottom: .3em; }\n.vs-feature-tabs-task { padding: .65em .7em; border-radius: .7em; font-weight: 600; line-height: 1.25; background: var(--c); color: #1a1d22; box-shadow: inset 0 0 0 1px rgba(0,0,0,.05); }\n.vs-feature-tabs-task small { display: block; margin-top: .3em; font-weight: 500; opacity: .6; }\n.vs-feature-tabs-task.is-drag { position: absolute; width: 17%; left: 47%; top: 44%; transform: rotate(-4deg); box-shadow: 0 14px 28px -10px rgba(20,30,50,.4), inset 0 0 0 1.5px var(--vs-feature-tabs-accent); }\n.vs-feature-tabs-cursor { position: absolute; width: 1.6em; height: 1.6em; left: 60%; top: 55%; filter: drop-shadow(0 2px 3px rgba(0,0,0,.25)); }\n/* 2: chart */\n.vs-feature-tabs-chart { position: relative; padding: 1.2em 1.4em 1em; }\n.vs-feature-tabs-chart svg { width: 100%; height: 100%; overflow: visible; }\n.vs-feature-tabs-chart .grid line { stroke: var(--vs-feature-tabs-line); }\n.vs-feature-tabs-chart .plan { fill: none; stroke: rgba(20,23,28,.3); stroke-width: 2; stroke-dasharray: 5 6; }\n.vs-feature-tabs-chart .real { fill: none; stroke: var(--vs-feature-tabs-accent); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }\n.vs-feature-tabs-chart .dot { fill: var(--vs-feature-tabs-media); stroke: var(--vs-feature-tabs-accent); stroke-width: 3; vector-effect: non-scaling-stroke; }\n.vs-feature-tabs-chart .risk { fill: color-mix(in srgb, var(--vs-feature-tabs-accent) 12%, transparent); }\n.vs-feature-tabs-callout { position: absolute; right: 8%; top: 22%; display: grid; gap: .3em; padding: .8em 1em; border-radius: .8em; background: #fff; box-shadow: 0 0 0 1px var(--vs-feature-tabs-line), 0 10px 24px -10px rgba(20,30,50,.35); }\n.vs-feature-tabs-callout b { font-weight: 700; }\n.vs-feature-tabs-callout span { color: var(--vs-feature-tabs-accent-ink); font-weight: 600; }\n/* 3: hand-off */\n.vs-feature-tabs-hand { position: relative; padding: 1.4em; display: grid; align-content: start; gap: .9em; }\n.vs-feature-tabs-card { display: grid; gap: .5em; padding: 1.1em 1.2em; border-radius: .9em; box-shadow: 0 0 0 1px var(--vs-feature-tabs-line); width: 62%; }\n.vs-feature-tabs-card b { font-size: 1.15em; }\n.vs-feature-tabs-card .vs-feature-tabs-chip { justify-self: start; margin-top: .2em; }\n.vs-feature-tabs-card p { margin: 0; color: var(--vs-feature-tabs-muted); line-height: 1.45; }\n.vs-feature-tabs-pop { position: absolute; left: 44%; top: 30%; width: 44%; padding: .5em; border-radius: .9em; background: #fff; box-shadow: 0 0 0 1px var(--vs-feature-tabs-line), 0 16px 36px -12px rgba(20,30,50,.35); }\n.vs-feature-tabs-pop div { display: flex; align-items: center; gap: .7em; padding: .55em .6em; border-radius: .6em; font-weight: 600; }\n.vs-feature-tabs-pop div em { margin-left: auto; font-style: normal; font-weight: 500; color: var(--vs-feature-tabs-muted); font-size: .9em; }\n.vs-feature-tabs-pop div.on { background: color-mix(in srgb, var(--vs-feature-tabs-accent) 10%, transparent); }\n.vs-feature-tabs-toast { position: absolute; left: 50%; bottom: 7%; transform: translateX(-50%); display: flex; align-items: center; gap: .6em; padding: .75em 1.1em; border-radius: 999px; white-space: nowrap;\n  background: #14171c; color: #fff; font-weight: 600; box-shadow: 0 12px 28px -10px rgba(0,0,0,.5); }\n.vs-feature-tabs-toast i { width: 1.5em; height: 1.5em; border-radius: 50%; display: grid; place-items: center; background: var(--vs-feature-tabs-accent); }\n/* 4: release notes */\n.vs-feature-tabs-doc { padding: 1.6em 2em; display: grid; align-content: start; gap: .8em; }\n.vs-feature-tabs-doc h4 { margin: 0; font-size: 1.7em; letter-spacing: -.02em; font-weight: 700; }\n.vs-feature-tabs-doc p { margin: 0; line-height: 1.55; color: rgba(20,23,28,.72); }\n.vs-feature-tabs-doc ul { margin: 0; padding-left: 1.2em; display: grid; gap: .45em; line-height: 1.45; }\n.vs-feature-tabs-doc li::marker { color: var(--vs-feature-tabs-accent); }\n.vs-feature-tabs-caret { display: inline-block; width: 2px; height: 1.1em; margin-left: 2px; vertical-align: -.15em; background: var(--vs-feature-tabs-accent); animation: vs-feature-tabs-blink 1s steps(1) infinite; }\n@keyframes vs-feature-tabs-blink { 50% { opacity: 0; } }\n\n@media (max-width: 760px) {\n  .vs-feature-tabs { grid-template-columns: 1fr; gap: 1.1rem; }\n  .vs-feature-tabs-list { grid-auto-flow: column; grid-auto-columns: 1fr; gap: .4rem; order: 0; }\n  .vs-feature-tabs-tab { grid-template-rows: auto 0fr 0; padding: .7rem .2rem .6rem; border: 0 !important; }\n  .vs-feature-tabs-tab[aria-selected=\"true\"] { grid-template-rows: auto 0fr 0; }\n  .vs-feature-tabs-title { font-size: .8125rem; gap: .35rem; flex-direction: column; align-items: flex-start; }\n  .vs-feature-tabs-bar { bottom: 0; background: var(--vs-feature-tabs-line); border-radius: 2px; }\n  .vs-feature-tabs-stage { aspect-ratio: 4 / 3.6; }\n  .vs-feature-tabs-m { font-size: .625rem; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .vs-feature-tabs-tab, .vs-feature-tabs-desc > span { transition: none; }\n  .vs-feature-tabs-caret { animation: none; }\n}\n</style>\n\n<div class=\"vs-feature-tabs\" data-interval=\"6000\">\n  <div class=\"vs-feature-tabs-list\" role=\"tablist\" aria-label=\"Product tour\" aria-orientation=\"vertical\">\n    <button class=\"vs-feature-tabs-tab\" role=\"tab\" type=\"button\" id=\"vs-ft-t1\" aria-controls=\"vs-ft-p1\" aria-selected=\"true\">\n      <span class=\"vs-feature-tabs-title\"><small>01</small>Plan the week</span>\n      <span class=\"vs-feature-tabs-desc\"><span>Drag work onto the days it will really happen. Capacity updates as you drop, so nobody is quietly at 140%.</span></span>\n      <span class=\"vs-feature-tabs-bar\" aria-hidden=\"true\"><i></i></span>\n    </button>\n    <button class=\"vs-feature-tabs-tab\" role=\"tab\" type=\"button\" id=\"vs-ft-t2\" aria-controls=\"vs-ft-p2\" aria-selected=\"false\" tabindex=\"-1\">\n      <span class=\"vs-feature-tabs-title\"><small>02</small>See what’s slipping</span>\n      <span class=\"vs-feature-tabs-desc\"><span>Plan against actual, drawn daily. The first project to drift gets circled before the stand-up, not after the deadline.</span></span>\n      <span class=\"vs-feature-tabs-bar\" aria-hidden=\"true\"><i></i></span>\n    </button>\n    <button class=\"vs-feature-tabs-tab\" role=\"tab\" type=\"button\" id=\"vs-ft-t3\" aria-controls=\"vs-ft-p3\" aria-selected=\"false\" tabindex=\"-1\">\n      <span class=\"vs-feature-tabs-title\"><small>03</small>Hand off in one move</span>\n      <span class=\"vs-feature-tabs-desc\"><span>Reassign a task with its context attached: the thread, the files, the last three decisions. They get a note, you get your afternoon.</span></span>\n      <span class=\"vs-feature-tabs-bar\" aria-hidden=\"true\"><i></i></span>\n    </button>\n    <button class=\"vs-feature-tabs-tab\" role=\"tab\" type=\"button\" id=\"vs-ft-t4\" aria-controls=\"vs-ft-p4\" aria-selected=\"false\" tabindex=\"-1\">\n      <span class=\"vs-feature-tabs-title\"><small>04</small>Notes that write themselves</span>\n      <span class=\"vs-feature-tabs-desc\"><span>Every merged change becomes a line in the release notes, in plain words your customers will actually read.</span></span>\n      <span class=\"vs-feature-tabs-bar\" aria-hidden=\"true\"><i></i></span>\n    </button>\n  </div>\n  <div class=\"vs-feature-tabs-stage\">\n    <div class=\"vs-feature-tabs-panel is-on\" role=\"tabpanel\" id=\"vs-ft-p1\" aria-labelledby=\"vs-ft-t1\">\n      <div class=\"vs-feature-tabs-m\">\n        <header>Week 44 <em>· Design team</em><span class=\"sp\"></span><span class=\"vs-feature-tabs-chip\" data-in>32 of 40 h planned</span></header>\n        <div class=\"vs-feature-tabs-week\">\n          <div class=\"vs-feature-tabs-day\"><b>Mon</b><div class=\"vs-feature-tabs-task\" style=\"--c:#cfe0ff\" data-in>Pricing page<small>Ines · 3h</small></div><div class=\"vs-feature-tabs-task\" style=\"--c:#d5f2e4\" data-in>Crit<small>Team · 1h</small></div></div>\n          <div class=\"vs-feature-tabs-day\"><b>Tue</b><div class=\"vs-feature-tabs-task\" style=\"--c:#ffe6b8\" data-in>Onboarding v2<small>Tomás · 5h</small></div></div>\n          <div class=\"vs-feature-tabs-day\"><b>Wed</b><div class=\"vs-feature-tabs-task\" style=\"--c:#ffd6cc\" data-in>Icon set<small>Mara · 2h</small></div><div class=\"vs-feature-tabs-task\" style=\"--c:#cfe0ff\" data-in>Checkout QA<small>Ines · 2h</small></div></div>\n          <div class=\"vs-feature-tabs-day\"><b>Thu</b></div>\n          <div class=\"vs-feature-tabs-day\"><b>Fri</b><div class=\"vs-feature-tabs-task\" style=\"--c:#d5f2e4\" data-in>Ship review<small>Team · 1h</small></div></div>\n          <div class=\"vs-feature-tabs-task is-drag\" style=\"--c:#ffe6b8\" data-in=\"drop\">Brand audit<small>Tomás · 4h</small></div>\n          <svg class=\"vs-feature-tabs-cursor\" viewBox=\"0 0 24 24\" data-in=\"drop\" aria-hidden=\"true\"><path d=\"M4 3l15 8-6.5 1.8L10 19z\" fill=\"#14171c\" stroke=\"#fff\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/></svg>\n        </div>\n      </div>\n    </div>\n    <div class=\"vs-feature-tabs-panel\" role=\"tabpanel\" id=\"vs-ft-p2\" aria-labelledby=\"vs-ft-t2\">\n      <div class=\"vs-feature-tabs-m\">\n        <header>Q4 roadmap <em>· Plan vs actual</em><span class=\"sp\"></span><span class=\"vs-feature-tabs-av\" style=\"background:#6b8cff\">IM</span><span class=\"vs-feature-tabs-av\" style=\"background:#2fa77a\">TR</span><span class=\"vs-feature-tabs-av\" style=\"background:#e0784d\">MK</span></header>\n        <div class=\"vs-feature-tabs-chart\">\n          <svg viewBox=\"0 0 400 220\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n            <g class=\"grid\"><line x1=\"0\" y1=\"40\" x2=\"400\" y2=\"40\"/><line x1=\"0\" y1=\"95\" x2=\"400\" y2=\"95\"/><line x1=\"0\" y1=\"150\" x2=\"400\" y2=\"150\"/><line x1=\"0\" y1=\"205\" x2=\"400\" y2=\"205\"/></g>\n            <path class=\"risk\" d=\"M200 128 L250 108 L300 88 L350 66 L400 44 L400 98 L350 112 L300 124 L250 132 L200 144 Z\" data-in/>\n            <path class=\"plan\" d=\"M0 200 L50 184 L100 167 L150 148 L200 128 L250 108 L300 88 L350 66 L400 44\"/>\n            <path class=\"real\" d=\"M0 200 L50 186 L100 172 L150 156 L200 144 L250 132 L300 124 L350 112 L400 98\" data-draw/>\n            <circle class=\"dot\" cx=\"400\" cy=\"98\" r=\"5\" data-in=\"drop\"/>\n          </svg>\n          <div class=\"vs-feature-tabs-callout\" data-in><b>API migration</b><span>3 days behind · 2 blockers</span></div>\n        </div>\n      </div>\n    </div>\n    <div class=\"vs-feature-tabs-panel\" role=\"tabpanel\" id=\"vs-ft-p3\" aria-labelledby=\"vs-ft-t3\">\n      <div class=\"vs-feature-tabs-m\">\n        <header>Checkout redesign <em>· 14 comments · 6 files</em></header>\n        <div class=\"vs-feature-tabs-hand\">\n          <div class=\"vs-feature-tabs-card\" data-in><b>Payment step, second pass</b><p>Card form now validates as you type. Needs a last look at the error states before Friday.</p><span class=\"vs-feature-tabs-chip\">Due Fri</span></div>\n          <div class=\"vs-feature-tabs-pop\" data-in>\n            <div><span class=\"vs-feature-tabs-av\" style=\"background:#2fa77a\">TR</span>Tomás Reyes<em>2 tasks</em></div>\n            <div class=\"on\"><span class=\"vs-feature-tabs-av\" style=\"background:#6b8cff\">IM</span>Ines Moreau<em>free today</em></div>\n            <div><span class=\"vs-feature-tabs-av\" style=\"background:#e0784d\">MK</span>Mara Kohl<em>on leave</em></div>\n          </div>\n          <div class=\"vs-feature-tabs-toast\" data-in=\"drop\"><i><svg viewBox=\"0 0 16 16\" width=\"10\" height=\"10\" aria-hidden=\"true\"><path d=\"m3.5 8.5 3 3 6-7\" fill=\"none\" stroke=\"#fff\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg></i>Handed to Ines, with the thread and files</div>\n        </div>\n      </div>\n    </div>\n    <div class=\"vs-feature-tabs-panel\" role=\"tabpanel\" id=\"vs-ft-p4\" aria-labelledby=\"vs-ft-t4\">\n      <div class=\"vs-feature-tabs-m\">\n        <header>Release notes <em>· draft</em><span class=\"sp\"></span><span class=\"vs-feature-tabs-chip\" data-in>From 23 merged changes</span></header>\n        <div class=\"vs-feature-tabs-doc\">\n          <h4 data-in>What’s new in 4.2</h4>\n          <p data-in>A faster week view, calmer notifications and one long-requested shortcut.</p>\n          <ul>\n            <li data-in>The week view opens in under a second, even with 400 tasks.</li>\n            <li data-in>Notifications now group by project, so Monday morning is one list, not forty.</li>\n            <li data-in>Press <b>H</b> on any task to hand it off<span class=\"vs-feature-tabs-caret\"></span></li>\n          </ul>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n\n<script>\n(() => {\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const fine = matchMedia('(hover: hover) and (pointer: fine)');\n  function mount(root) {\n    if (root.vsFeatureTabs) return root.vsFeatureTabs;\n    const tabs = [...root.querySelectorAll('[role=\"tab\"]')], panels = tabs.map(t => document.getElementById(t.getAttribute('aria-controls')));\n    const bars = tabs.map(t => t.querySelector('.vs-feature-tabs-bar i')), stage = root.querySelector('.vs-feature-tabs-stage');\n    let cur = Math.max(0, tabs.findIndex(t => t.getAttribute('aria-selected') === 'true')), t = 0, raf = 0, last = 0;\n    let hover = false, focus = false, visible = true;\n    const interval = () => +root.dataset.interval || 6000;\n    const auto = () => root.dataset.autoplay !== 'false' && !reduce.matches;\n    const held = () => hover || focus;\n\n    const enter = p => {\n      p.querySelectorAll('[data-in]').forEach((el, i) => {\n        const drop = el.dataset.in === 'drop';\n        el.animate(drop ? [{ opacity: 0, transform: `${getComputedStyle(el).transform === 'none' ? '' : getComputedStyle(el).transform} translateY(-14px) scale(1.06)`.trim() }, { opacity: 1 }]\n                        : [{ opacity: 0, transform: 'translateY(10px)', filter: 'blur(3px)' }, { opacity: 1, transform: 'none', filter: 'blur(0)' }],\n          { duration: drop ? 620 : 560, delay: drop ? 520 : 140 + i * 55, easing: drop ? 'cubic-bezier(.3,1.5,.5,1)' : 'cubic-bezier(.16,1,.3,1)', fill: 'backwards' });\n      });\n      p.querySelectorAll('[data-draw]').forEach(el => {\n        const len = el.getTotalLength ? el.getTotalLength() : 600;\n        el.animate([{ strokeDasharray: `${len} ${len}`, strokeDashoffset: len }, { strokeDasharray: `${len} ${len}`, strokeDashoffset: 0 }], { duration: 1200, delay: 180, easing: 'cubic-bezier(.45,0,.2,1)', fill: 'backwards' });\n      });\n    };\n    const select = (i, { user = false, focusTab = false } = {}) => {\n      i = (i + tabs.length) % tabs.length; if (i === cur) { if (user) t = 0; return; }\n      const from = panels[cur], to = panels[i], dir = i > cur ? 1 : -1;\n      tabs[cur].setAttribute('aria-selected', 'false'); tabs[cur].tabIndex = -1; bars[cur].style.transform = 'scaleX(0)';\n      tabs[i].setAttribute('aria-selected', 'true'); tabs[i].tabIndex = 0; if (focusTab) tabs[i].focus();\n      cur = i; t = 0;\n      from.getAnimations().forEach(a => a.cancel()); to.getAnimations().forEach(a => a.cancel());\n      to.classList.add('is-on');\n      if (reduce.matches) {\n        to.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 250 });\n        from.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 250 }).onfinish = () => from.classList.remove('is-on');\n      } else {\n        from.style.zIndex = 0; to.style.zIndex = 1;\n        from.animate([{ opacity: 1, transform: 'none', filter: 'blur(0)' }, { opacity: 0, transform: `translateY(${-16 * dir}px) scale(.965)`, filter: 'blur(6px)' }],\n          { duration: 420, easing: 'cubic-bezier(.4,0,.6,1)', fill: 'forwards' }).onfinish = () => { if (cur !== panels.indexOf(from)) from.classList.remove('is-on'); from.getAnimations().forEach(a => a.cancel()); };\n        to.animate([{ opacity: 0, transform: `translateY(${28 * dir}px) scale(1.015)`, filter: 'blur(8px)' }, { opacity: 1, transform: 'none', filter: 'blur(0)' }],\n          { duration: 760, delay: 90, easing: 'cubic-bezier(.16,1,.3,1)', fill: 'backwards' });\n        enter(to);\n      }\n      root.dispatchEvent(new CustomEvent('vs-feature-tabs:change', { bubbles: true, detail: { index: i } }));\n      wake();\n    };\n    const tick = now => {\n      const dt = Math.min(.05, (now - last) / 1000 || .016); last = now;\n      root.classList.toggle('is-held', held() && auto());\n      if (auto() && !held()) { t += dt * 1000; if (t >= interval()) select(cur + 1); }\n      bars[cur].style.transform = `scaleX(${auto() ? Math.min(1, t / interval()).toFixed(4) : 1})`;\n      raf = visible && auto() ? requestAnimationFrame(tick) : 0;\n    };\n    const wake = () => { if (!raf && visible) { last = performance.now(); raf = requestAnimationFrame(tick); } };\n\n    tabs.forEach((tab, i) => tab.addEventListener('click', () => select(i, { user: true })));\n    root.querySelector('[role=\"tablist\"]').addEventListener('keydown', e => {\n      const k = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[e.key];\n      if (k) { e.preventDefault(); select(cur + k, { user: true, focusTab: true }); }\n      if (e.key === 'Home') { e.preventDefault(); select(0, { user: true, focusTab: true }); }\n      if (e.key === 'End') { e.preventDefault(); select(tabs.length - 1, { user: true, focusTab: true }); }\n    });\n    root.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse' && fine.matches) hover = true; });\n    root.addEventListener('pointerleave', () => { hover = false; });\n    root.addEventListener('focusin', () => { focus = true; });\n    root.addEventListener('focusout', e => { if (!root.contains(e.relatedTarget)) focus = false; });\n    // swipe the media on touch\n    let sx = null;\n    stage.addEventListener('pointerdown', e => { if (e.pointerType !== 'mouse') sx = [e.clientX, e.clientY]; });\n    stage.addEventListener('pointerup', e => { if (!sx) return; const dx = e.clientX - sx[0], dy = e.clientY - sx[1]; sx = null; if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy) * 1.4) select(cur + (dx < 0 ? 1 : -1), { user: true }); });\n    stage.addEventListener('pointercancel', () => { sx = null; });\n    new IntersectionObserver(e => { visible = e[0].isIntersecting; wake(); }).observe(root);\n    new MutationObserver(wake).observe(root, { attributes: true, attributeFilter: ['data-autoplay', 'data-interval'] });\n    panels.forEach((p, i) => p.classList.toggle('is-on', i === cur));\n    enter(panels[cur]); wake();\n    const api = { select: i => select(i, { user: true }), get index() { return cur; } };\n    root.vsFeatureTabs = api; return api;\n  }\n  const init = () => document.querySelectorAll('.vs-feature-tabs').forEach(mount);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n  window.vsFeatureTabs = { mount };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Marketing blocks",
    "tags": [
      "tabs",
      "features",
      "product tour",
      "autoplay",
      "progress",
      "media swap",
      "showcase",
      "aria tabs"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#d99a1e",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/feature-tabs/index.html",
    "thumbnail": "https://vividsites.app/api/component/feature-tabs/thumb.jpg",
    "video": "https://vividsites.app/api/component/feature-tabs/thumb.mp4",
    "page": "https://vividsites.app/c/feature-tabs"
  }
}