# Bouncy Content Tabs

> Springy tab panels with a playful bounce as content switches — clear hierarchy for product and marketing UI.

**Definition:** Bouncy Content Tabs is a text UI animation from collections.html. It is built with HTML, CSS, JavaScript, and GSAP, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Text
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-01
- Canonical page: https://html.bosconvertpro.com/collection/bouncy-content-tabs
- Full-page preview: https://html.bosconvertpro.com/collection/bouncy-content-tabs/full
- Live demo: https://html.bosconvertpro.com/demos/bouncy-content-tabs/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/bouncy-content-tabs/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/bouncy-content-tabs/full to preview the animation in full screen.
2. Copy index.html, styles.css, and script.js from the code tabs on https://html.bosconvertpro.com/collection/bouncy-content-tabs.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Bouncy Content Tabs?

Bouncy Content Tabs is a text UI animation from collections.html. Springy tab panels with a playful bounce as content switches — clear hierarchy for product and marketing UI.

### What is Bouncy Content Tabs built with?

Bouncy Content Tabs is built with HTML, CSS, JavaScript, and GSAP. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use Bouncy Content Tabs in my project?

Open the animation page (https://html.bosconvertpro.com/collection/bouncy-content-tabs), preview it live, then copy index.html, styles.css, and script.js from the code tabs. Paste the files into your project and adjust timing, easing, and content as needed. A standalone demo runs at https://html.bosconvertpro.com/demos/bouncy-content-tabs/index.html.

### Is Bouncy Content Tabs free to use?

Yes. The Bouncy Content Tabs demo code may be used in personal and commercial projects. You may not resell or redistribute the collection itself as a competing template pack. Full terms: https://html.bosconvertpro.com/terms.

## Source — index.html
````html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Bouncy Content Tabs — Destinations</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link
      href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=Outfit:wght@300;400;500;600&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <main class="page">
      <div data-bouncy-tabs-init class="bouncy-tabs">
        <header class="bouncy-tabs__header">
          <p class="bouncy-tabs__eyebrow">Curated escapes</p>
          <h1 class="bouncy-tabs__title">Choose your city</h1>
        </header>

        <div data-bouncy-tabs-nav role="tablist" aria-label="Cities" class="bouncy-tabs__nav">
          <div data-bouncy-tabs-ghost class="bouncy-tabs__ghost" aria-hidden="true"></div>
          <div data-bouncy-tabs-indicator class="bouncy-tabs__indicator" aria-hidden="true"></div>
          <button data-bouncy-tabs-button data-active role="tab" type="button" class="bouncy-tabs__button">
            Paris
          </button>
          <button data-bouncy-tabs-button role="tab" type="button" class="bouncy-tabs__button">
            Tokyo
          </button>
          <button data-bouncy-tabs-button role="tab" type="button" class="bouncy-tabs__button">
            New York
          </button>
          <button data-bouncy-tabs-button role="tab" type="button" class="bouncy-tabs__button">
            Dubai
          </button>
          <button data-bouncy-tabs-button role="tab" type="button" class="bouncy-tabs__button">
            London
          </button>
        </div>

        <div data-bouncy-tabs-card class="bouncy-tabs__collection">
          <div data-bouncy-tabs-panels class="bouncy-tabs__list">
            <!-- Paris -->
            <div data-active data-bouncy-tabs-panel role="tabpanel" class="bouncy-tabs__item">
              <div class="bouncy-tabs__item-text">
                <span class="bouncy-tabs__meta">France · Europe</span>
                <h3 class="bouncy-tabs__heading">City of light, after dark</h3>
                <p class="bouncy-tabs__para">
                  Cobblestone evenings along the Seine, espresso at a zinc bar,
                  and galleries that still reward a long pause. Paris is less a
                  checklist than a mood — refined, restless, endlessly rewatchable.
                </p>
                <a href="#" class="bouncy-tabs__cta"><span>Explore Paris</span></a>
              </div>
              <div class="bouncy-tabs__item-visual">
                <img
                  src="https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&w=1000&q=85"
                  alt="Eiffel Tower in Paris under a clear sky"
                  class="bouncy-tabs__item-img"
                  draggable="false"
                />
              </div>
            </div>

            <!-- Tokyo -->
            <div data-bouncy-tabs-panel role="tabpanel" class="bouncy-tabs__item">
              <div class="bouncy-tabs__item-text">
                <span class="bouncy-tabs__meta">Japan · Asia</span>
                <h3 class="bouncy-tabs__heading">Neon, ritual, precision</h3>
                <p class="bouncy-tabs__para">
                  Shibuya’s pulse, temple quiet at dawn, and a food culture that
                  treats craft like ceremony. Tokyo moves fast — but always with
                  intention, polish, and a sense of tomorrow.
                </p>
                <a href="#" class="bouncy-tabs__cta"><span>Explore Tokyo</span></a>
              </div>
              <div class="bouncy-tabs__item-visual">
                <img
                  src="https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?auto=format&fit=crop&w=1000&q=85"
                  alt="Tokyo city skyline with neon lights at dusk"
                  class="bouncy-tabs__item-img"
                  draggable="false"
                />
              </div>
            </div>

            <!-- New York -->
            <div data-bouncy-tabs-panel role="tabpanel" class="bouncy-tabs__item">
              <div class="bouncy-tabs__item-text">
                <span class="bouncy-tabs__meta">USA · North America</span>
                <h3 class="bouncy-tabs__heading">Vertical ambition</h3>
                <p class="bouncy-tabs__para">
                  Street-level energy and skyline drama in the same block. From
                  brownstone mornings to late-night jazz, New York rewards
                  anyone willing to walk one more avenue.
                </p>
                <a href="#" class="bouncy-tabs__cta"><span>Explore New York</span></a>
              </div>
              <div class="bouncy-tabs__item-visual">
                <img
                  src="https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?auto=format&fit=crop&w=1000&q=85"
                  alt="New York City skyline with Empire State Building"
                  class="bouncy-tabs__item-img"
                  draggable="false"
                />
              </div>
            </div>

            <!-- Dubai -->
            <div data-bouncy-tabs-panel role="tabpanel" class="bouncy-tabs__item">
              <div class="bouncy-tabs__item-text">
                <span class="bouncy-tabs__meta">UAE · Middle East</span>
                <h3 class="bouncy-tabs__heading">Desert meets future</h3>
                <p class="bouncy-tabs__para">
                  Glass towers above warm sand, marina light on still water, and
                  hospitality elevated to an art form. Dubai is spectacle with
                  substance — bold architecture and quiet luxury side by side.
                </p>
                <a href="#" class="bouncy-tabs__cta"><span>Explore Dubai</span></a>
              </div>
              <div class="bouncy-tabs__item-visual">
                <img
                  src="https://images.unsplash.com/photo-1512453979798-5ea266f8880c?auto=format&fit=crop&w=1000&q=85"
                  alt="Dubai skyline with Burj Khalifa at sunset"
                  class="bouncy-tabs__item-img"
                  draggable="false"
                />
              </div>
            </div>

            <!-- London -->
            <div data-bouncy-tabs-panel role="tabpanel" class="bouncy-tabs__item">
              <div class="bouncy-tabs__item-text">
                <span class="bouncy-tabs__meta">United Kingdom · Europe</span>
                <h3 class="bouncy-tabs__heading">Heritage with edge</h3>
                <p class="bouncy-tabs__para">
                  Royal parks, brick terraces, and a creative undercurrent that
                  never sleeps. London layers centuries of history with sharp
                  contemporary culture — polished, witty, always in season.
                </p>
                <a href="#" class="bouncy-tabs__cta"><span>Explore London</span></a>
              </div>
              <div class="bouncy-tabs__item-visual">
                <img
                  src="https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?auto=format&fit=crop&w=1000&q=85"
                  alt="London Big Ben and Westminster bridge at dusk"
                  class="bouncy-tabs__item-img"
                  draggable="false"
                />
              </div>
            </div>
          </div>
        </div>
      </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
````

## Source — styles.css
````css
:root {
  --bg: #050505;
  --bg-elevated: #0c0c0c;
  --card: #111111;
  --card-border: rgba(255, 255, 255, 0.07);
  --ink: #f4f0e8;
  --ink-soft: rgba(244, 240, 232, 0.62);
  --ink-muted: rgba(244, 240, 232, 0.42);
  --gold: #c9a66b;
  --gold-soft: #e4c99a;
  --gold-deep: #a8864a;
  --nav-bg: rgba(255, 255, 255, 0.05);
  --nav-border: rgba(255, 255, 255, 0.08);
  --ghost-bg: rgba(255, 255, 255, 0.08);
  --indicator: linear-gradient(135deg, #d4b078 0%, #c9a66b 45%, #a8864a 100%);
  --font: "Outfit", system-ui, -apple-system, sans-serif;
  --font-display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --shadow-card:
    0 0 0 1px rgba(255, 255, 255, 0.04),
    0 24px 64px rgba(0, 0, 0, 0.55),
    0 4px 16px rgba(0, 0, 0, 0.35);
  --glow: 0 0 48px rgba(201, 166, 107, 0.12);
}

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  width: 100%;
  height: 100%;
}

body {
  font-family: var(--font);
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

button {
  font: inherit;
  cursor: pointer;
}

img {
  display: block;
  max-width: 100%;
}

a {
  color: inherit;
  text-decoration: none;
}

/* ---------- Page shell ---------- */

.page {
  position: relative;
  width: 100%;
  min-height: 100%;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3.5em 1.5em;
  overflow: hidden;
  background:
    radial-gradient(ellipse 70% 50% at 50% -10%, rgba(201, 166, 107, 0.1), transparent 55%),
    radial-gradient(ellipse 50% 40% at 80% 100%, rgba(201, 166, 107, 0.05), transparent 50%),
    radial-gradient(ellipse 40% 30% at 10% 80%, rgba(255, 255, 255, 0.03), transparent 45%),
    var(--bg);
}

.page::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
  opacity: 0.45;
  pointer-events: none;
  mix-blend-mode: overlay;
}

/* ---------- Bouncy tabs ---------- */

.bouncy-tabs {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 2.25em;
  width: min(100%, 52em);
}

.bouncy-tabs__header {
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.45em;
}

.bouncy-tabs__eyebrow {
  font-size: 0.72em;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold);
}

.bouncy-tabs__title {
  font-family: var(--font-display);
  font-size: clamp(2rem, 4vw, 2.75rem);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--ink);
  font-style: italic;
}

/* ---------- Nav ---------- */

.bouncy-tabs__nav {
  position: relative;
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  padding: 0.4em;
  border-radius: 100em;
  background-color: var(--nav-bg);
  border: 1px solid var(--nav-border);
  box-shadow:
    0 8px 32px rgba(0, 0, 0, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.bouncy-tabs__ghost {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  width: 2em;
  border-radius: 100em;
  background-color: var(--ghost-bg);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.bouncy-tabs__indicator {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  width: 3em;
  border-radius: 100em;
  background: var(--indicator);
  box-shadow:
    0 2px 12px rgba(201, 166, 107, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.25);
  pointer-events: none;
  will-change: transform, width, height;
}

.bouncy-tabs__button {
  position: relative;
  z-index: 3;
  appearance: none;
  border: none;
  border-radius: 100em;
  padding: 0.7em 1.35em;
  background: transparent;
  color: var(--ink-muted);
  font-size: 0.92em;
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1.3;
  transition: color 0.3s ease;
  white-space: nowrap;
}

.bouncy-tabs__button:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

[data-bouncy-tabs-button][data-active] {
  color: #1a140c;
}

/* ---------- Content card ---------- */

.bouncy-tabs__collection {
  width: 100%;
  padding: 2.75em;
  border-radius: 1.6em;
  background:
    linear-gradient(160deg, rgba(255, 255, 255, 0.04) 0%, transparent 40%),
    var(--card);
  border: 1px solid var(--card-border);
  box-shadow: var(--shadow-card), var(--glow);
  overflow: hidden;
  will-change: transform;
}

.bouncy-tabs__list {
  position: relative;
  width: 100%;
}

.bouncy-tabs__item {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 2.25em;
}

[data-bouncy-tabs-panel]:not([data-active]) {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

[data-bouncy-tabs-panel][data-active] {
  pointer-events: auto;
}

.bouncy-tabs__item-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  flex: 1;
  gap: 0.85em;
  min-width: 0;
}

.bouncy-tabs__meta {
  font-size: 0.72em;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold);
}

.bouncy-tabs__heading {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.65rem, 3vw, 2.1rem);
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.bouncy-tabs__para {
  margin: 0;
  font-size: 0.98em;
  font-weight: 300;
  line-height: 1.55;
  color: var(--ink-soft);
  max-width: 32ch;
}

.bouncy-tabs__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  margin-top: 0.5em;
  padding: 0.8em 1.6em;
  border-radius: 100em;
  background: var(--indicator);
  color: #1a140c;
  font-size: 0.9em;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.4;
  box-shadow:
    0 4px 20px rgba(201, 166, 107, 0.28),
    inset 0 1px 0 rgba(255, 255, 255, 0.28);
  transition:
    transform 0.25s ease,
    box-shadow 0.25s ease,
    opacity 0.25s ease;
}

.bouncy-tabs__cta:hover {
  transform: translateY(-1px) scale(1.02);
  box-shadow:
    0 8px 28px rgba(201, 166, 107, 0.38),
    inset 0 1px 0 rgba(255, 255, 255, 0.3);
}

.bouncy-tabs__cta:active {
  transform: scale(0.98);
}

.bouncy-tabs__cta:focus-visible {
  outline: 2px solid var(--gold-soft);
  outline-offset: 3px;
}

.bouncy-tabs__item-visual {
  position: relative;
  width: 46%;
  flex-shrink: 0;
  aspect-ratio: 1;
  border-radius: 1.15em;
  overflow: hidden;
  background: var(--bg-elevated);
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.06),
    0 16px 40px rgba(0, 0, 0, 0.4);
}

.bouncy-tabs__item-visual::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    145deg,
    rgba(201, 166, 107, 0.12) 0%,
    transparent 40%,
    rgba(0, 0, 0, 0.25) 100%
  );
  pointer-events: none;
  z-index: 1;
}

.bouncy-tabs__item-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  user-select: none;
  -webkit-user-drag: none;
  transform: scale(1.02);
  transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}

[data-bouncy-tabs-panel][data-active] .bouncy-tabs__item-img {
  transform: scale(1);
}

/* ---------- Responsive ---------- */

@media screen and (max-width: 767px) {
  .page {
    padding: 2.5em 1.15em 2em;
    align-items: flex-start;
  }

  .bouncy-tabs {
    gap: 1.75em;
  }

  .bouncy-tabs__nav {
    border-radius: 1.35em;
  }

  .bouncy-tabs__button {
    padding: 0.58em 1.05em;
    font-size: 0.88em;
  }

  .bouncy-tabs__collection {
    padding: 1.4em;
    border-radius: 1.25em;
  }

  .bouncy-tabs__item {
    flex-direction: column;
    align-items: stretch;
    gap: 1.4em;
  }

  .bouncy-tabs__item-visual {
    width: 100%;
    order: -1;
    aspect-ratio: 16 / 11;
  }

  .bouncy-tabs__heading {
    font-size: 1.55rem;
  }

  .bouncy-tabs__para {
    max-width: none;
    font-size: 0.95em;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bouncy-tabs__button,
  .bouncy-tabs__cta,
  .bouncy-tabs__item-img {
    transition: none;
  }
}
````

## Source — script.js
````javascript
/**
 * Bouncy Content Tabs
 * Pill indicator with overshoot + squash, hover ghost, and card panel transitions.
 */

function initBouncyContentTabs() {
  const config = {
    travel: 0.35,
    settle: 0.25,
    squashDown: 0.15,
    squashBack: 0.45,
    overshoot: 16,
    cardSquash: 0.2,
    cardInflate: 0.45,
  };

  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
  let instanceCount = 0;

  document.querySelectorAll("[data-bouncy-tabs-init]").forEach((root) => {
    const nav = root.querySelector("[data-bouncy-tabs-nav]");
    const indicator = root.querySelector("[data-bouncy-tabs-indicator]");
    const ghost = root.querySelector("[data-bouncy-tabs-ghost]");
    const buttons = nav
      ? Array.from(nav.querySelectorAll("[data-bouncy-tabs-button]"))
      : [];

    if (!nav || !indicator || buttons.length < 2) return;

    const card = root.querySelector("[data-bouncy-tabs-card]");
    const panelsWrap = root.querySelector("[data-bouncy-tabs-panels]");
    const panels = panelsWrap
      ? Array.from(panelsWrap.querySelectorAll("[data-bouncy-tabs-panel]"))
      : [];
    const hasCard = Boolean(card && panelsWrap && panels.length);

    instanceCount += 1;

    // Wire ARIA relationships between tabs and panels
    buttons.forEach((button, i) => {
      const panel = panels[i];
      if (!panel) return;
      if (!button.id) {
        button.id = "bouncy-tabs-" + instanceCount + "-tab-" + (i + 1);
      }
      if (!panel.id) {
        panel.id = "bouncy-tabs-" + instanceCount + "-panel-" + (i + 1);
      }
      button.setAttribute("aria-controls", panel.id);
      panel.setAttribute("aria-labelledby", button.id);
    });

    let activeIndex = Math.max(
      0,
      buttons.findIndex((b) => b.hasAttribute("data-active"))
    );
    const pos = { left: 0, right: 0 };
    let ghostVisible = false;

    const dur = (d) => (reduceMotion.matches ? 0 : d);

    function syncButtonState() {
      buttons.forEach((b, i) => {
        b.toggleAttribute("data-active", i === activeIndex);
        b.setAttribute("aria-selected", i === activeIndex ? "true" : "false");
        b.tabIndex = i === activeIndex ? 0 : -1;
      });
    }

    function rectFor(button) {
      return {
        left: button.offsetLeft,
        right: button.offsetLeft + button.offsetWidth,
        top: button.offsetTop,
        height: button.offsetHeight,
      };
    }

    function render() {
      gsap.set(indicator, {
        x: pos.left,
        width: pos.right - pos.left,
      });
    }

    function place(index) {
      const target = rectFor(buttons[index]);
      pos.left = target.left;
      pos.right = target.right;
      gsap.killTweensOf(pos);
      gsap.killTweensOf(indicator);
      gsap.set(indicator, {
        scaleY: 1,
        y: target.top,
        height: target.height,
      });
      render();
    }

    function setPanelsHeight() {
      if (hasCard) {
        gsap.set(panelsWrap, { height: panels[activeIndex].offsetHeight });
      }
    }

    function swapPanelsInstant() {
      if (!hasCard) return;
      panels.forEach((p, i) => {
        gsap.killTweensOf(p);
        gsap.set(p, { autoAlpha: i === activeIndex ? 1 : 0, x: 0 });
        p.toggleAttribute("data-active", i === activeIndex);
      });
      gsap.set(card, { scaleX: 1, scaleY: 1, x: 0 });
      setPanelsHeight();
    }

    function transitionCard(prevIndex, movingRight) {
      if (!hasCard) return;

      const direction = movingRight ? 1 : -1;
      const incoming = panels[activeIndex];
      const outgoing = panels[prevIndex];
      if (!incoming || !outgoing) return;

      panels.forEach((p) => {
        if (p !== incoming && p !== outgoing) {
          gsap.killTweensOf(p);
          gsap.set(p, { autoAlpha: 0, x: 0 });
          p.removeAttribute("data-active");
        }
      });

      incoming.setAttribute("data-active", "");
      outgoing.removeAttribute("data-active");

      gsap.killTweensOf([card, panelsWrap, incoming, outgoing]);
      gsap.set(card, { transformOrigin: "50% 50%" });

      const targetHeight = incoming.offsetHeight;

      gsap
        .timeline()
        .to(
          card,
          {
            scaleX: 0.96,
            scaleY: 1.02,
            x: direction * 10,
            duration: config.cardSquash,
            ease: "power2.out",
          },
          0
        )
        .to(
          outgoing,
          {
            x: direction * -35,
            autoAlpha: 0,
            duration: 0.2,
            ease: "power2.out",
          },
          0
        )
        .to(
          panelsWrap,
          {
            height: targetHeight,
            duration: 0.3,
            ease: "power2.inOut",
          },
          0.1
        )
        .fromTo(
          incoming,
          { x: direction * 35, autoAlpha: 0 },
          {
            x: 0,
            autoAlpha: 1,
            duration: 0.3,
            ease: "power2.out",
          },
          config.cardSquash
        )
        .to(
          card,
          {
            scaleX: 1,
            scaleY: 1,
            x: 0,
            duration: config.cardInflate,
            ease: "back.out(2)",
          },
          config.cardSquash
        )
        .set(outgoing, { x: 0 });
    }

    function goTo(index) {
      if (index === activeIndex) return;

      const prevIndex = activeIndex;
      const movingRight = index > activeIndex;
      activeIndex = index;
      syncButtonState();

      if (reduceMotion.matches) {
        place(index);
        swapPanelsInstant();
        return;
      }

      transitionCard(prevIndex, movingRight);

      const target = rectFor(buttons[index]);
      gsap.killTweensOf(pos);
      gsap.killTweensOf(indicator);

      const hDir =
        target.left === pos.left
          ? movingRight
            ? 1
            : -1
          : target.left > pos.left
            ? 1
            : -1;

      const distance = Math.abs(target.left - pos.left);
      const maxOvershoot = Math.max(6, config.overshoot);
      let overshoot =
        gsap.utils.clamp(6, maxOvershoot, distance * 0.08) * hDir;

      if (hDir > 0) {
        overshoot = Math.min(overshoot, nav.clientWidth - target.right);
      } else {
        overshoot = Math.max(overshoot, -target.left);
      }

      gsap.set(indicator, { transformOrigin: "50% 50%" });

      // Vertical align (for wrapped nav rows)
      gsap.to(indicator, {
        y: target.top,
        height: target.height,
        duration: config.travel,
        ease: "power3.out",
      });

      // Horizontal travel with overshoot, then settle
      gsap
        .timeline()
        .to(pos, {
          left: target.left + overshoot,
          right: target.right + overshoot,
          duration: config.travel,
          ease: "power3.out",
          onUpdate: render,
        })
        .to(pos, {
          left: target.left,
          right: target.right,
          duration: config.settle,
          ease: "power2.inOut",
          onUpdate: render,
        });

      // Squash & bounce of the pill
      gsap
        .timeline()
        .to(indicator, {
          scaleY: 0.78,
          duration: config.squashDown,
          ease: "power2.out",
        })
        .to(indicator, {
          scaleY: 1,
          duration: config.squashBack,
          ease: "back.out(2.5)",
        });
    }

    function moveGhost(button) {
      if (!ghost) return;
      const target = rectFor(button);

      if (!ghostVisible) {
        ghostVisible = true;
        gsap.killTweensOf(ghost);
        gsap.set(ghost, {
          x: target.left,
          y: target.top,
          width: target.right - target.left,
          height: target.height,
        });
        gsap.to(ghost, {
          autoAlpha: 1,
          duration: dur(0.25),
          ease: "power2.out",
        });
      } else {
        gsap.to(ghost, {
          x: target.left,
          y: target.top,
          width: target.right - target.left,
          height: target.height,
          autoAlpha: 1,
          duration: dur(0.3),
          ease: "power3.out",
        });
      }
    }

    function hideGhost() {
      if (!ghost) return;
      ghostVisible = false;
      gsap.to(ghost, {
        autoAlpha: 0,
        duration: dur(0.2),
        ease: "power1.out",
      });
    }

    function refreshLayout() {
      const rows = new Set(buttons.map((b) => b.offsetTop));
      nav.toggleAttribute("data-wrapped", rows.size > 1);
      place(activeIndex);
      setPanelsHeight();
    }

    // Events
    buttons.forEach((button, index) => {
      button.addEventListener("click", () => goTo(index));
      button.addEventListener("mouseenter", () => moveGhost(button));
    });

    nav.addEventListener("mouseleave", hideGhost);

    nav.addEventListener("keydown", (event) => {
      let next = null;
      if (event.key === "ArrowRight") {
        next = Math.min(buttons.length - 1, activeIndex + 1);
      }
      if (event.key === "ArrowLeft") {
        next = Math.max(0, activeIndex - 1);
      }
      if (event.key === "Home") next = 0;
      if (event.key === "End") next = buttons.length - 1;
      if (next === null) return;

      event.preventDefault();
      if (next !== activeIndex) {
        goTo(next);
        buttons[next].focus();
      }
    });

    window.addEventListener("resize", refreshLayout);

    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(refreshLayout);
    }

    // Initial state
    if (ghost) gsap.set(ghost, { autoAlpha: 0 });
    panels.forEach((p, i) => {
      gsap.set(p, { autoAlpha: i === activeIndex ? 1 : 0 });
    });

    syncButtonState();
    refreshLayout();
  });
}

document.addEventListener("DOMContentLoaded", () => {
  initBouncyContentTabs();
});
````
