# Morphing Tabs

> Browser-style tabs with a liquid SVG surface that glides between panels, drag reorder, and close — workspace and multi-space UI.

**Definition:** Morphing 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-07
- Canonical page: https://html.bosconvertpro.com/collection/morphing-tabs
- Full-page preview: https://html.bosconvertpro.com/collection/morphing-tabs/full
- Live demo: https://html.bosconvertpro.com/demos/morphing-tabs/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/morphing-tabs/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/morphing-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/morphing-tabs.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Morphing Tabs?

Morphing Tabs is a text UI animation from collections.html. Browser-style tabs with a liquid SVG surface that glides between panels, drag reorder, and close — workspace and multi-space UI.

### What is Morphing Tabs built with?

Morphing 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 Morphing Tabs in my project?

Open the animation page (https://html.bosconvertpro.com/collection/morphing-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/morphing-tabs/index.html.

### Is Morphing Tabs free to use?

Yes. The Morphing 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>Morphing Tabs</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <main class="page">
      <div class="morph-tabs" data-morph-tabs data-active="focus">
        <svg
          class="morph-tabs__surface"
          data-surface
          aria-hidden="true"
          focusable="false"
          preserveAspectRatio="none"
        >
          <path data-surface-path fill="currentColor"></path>
        </svg>

        <div class="morph-tabs__rail" data-rail role="tablist" aria-label="Spaces">
          <div class="morph-tabs__tab is-active" data-tab data-id="focus">
            <div class="morph-tabs__tab-box">
              <span class="morph-tabs__hover" aria-hidden="true"></span>
              <button
                class="morph-tabs__btn"
                type="button"
                role="tab"
                id="tab-focus"
                aria-controls="panel-spaces"
                aria-selected="true"
                tabindex="0"
              >
                <span class="morph-tabs__label">Focus</span>
              </button>
              <button
                class="morph-tabs__close"
                type="button"
                data-close
                aria-label="Close Focus"
              >
                <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
                  <path
                    d="M4 4l8 8M12 4l-8 8"
                    stroke="currentColor"
                    stroke-width="1.5"
                    stroke-linecap="round"
                  />
                </svg>
              </button>
            </div>
          </div>

          <div class="morph-tabs__tab" data-tab data-id="shared">
            <div class="morph-tabs__tab-box">
              <span class="morph-tabs__hover" aria-hidden="true"></span>
              <button
                class="morph-tabs__btn"
                type="button"
                role="tab"
                id="tab-shared"
                aria-controls="panel-spaces"
                aria-selected="false"
                tabindex="-1"
              >
                <span class="morph-tabs__label">Shared</span>
              </button>
              <button
                class="morph-tabs__close"
                type="button"
                data-close
                aria-label="Close Shared"
              >
                <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
                  <path
                    d="M4 4l8 8M12 4l-8 8"
                    stroke="currentColor"
                    stroke-width="1.5"
                    stroke-linecap="round"
                  />
                </svg>
              </button>
            </div>
          </div>

          <div class="morph-tabs__tab" data-tab data-id="archive">
            <div class="morph-tabs__tab-box">
              <span class="morph-tabs__hover" aria-hidden="true"></span>
              <button
                class="morph-tabs__btn"
                type="button"
                role="tab"
                id="tab-archive"
                aria-controls="panel-spaces"
                aria-selected="false"
                tabindex="-1"
              >
                <span class="morph-tabs__label">Archive</span>
              </button>
              <button
                class="morph-tabs__close"
                type="button"
                data-close
                aria-label="Close Archive"
              >
                <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
                  <path
                    d="M4 4l8 8M12 4l-8 8"
                    stroke="currentColor"
                    stroke-width="1.5"
                    stroke-linecap="round"
                  />
                </svg>
              </button>
            </div>
          </div>
        </div>

        <div
          class="morph-tabs__panel"
          id="panel-spaces"
          role="tabpanel"
          aria-labelledby="tab-focus"
          data-panel
        >
          <div
            class="morph-tabs__content is-active"
            data-content
            data-id="focus"
          >
            <div class="mt-top">
              <span class="morph-tabs__badge" style="--accent: #6366f1"
                >Deep work</span
              >
              <span class="mt-live"><i></i> In session</span>
            </div>
            <h2 class="morph-tabs__title">Focus</h2>
            <p class="morph-tabs__detail">
              A quiet space for the work that needs room to breathe.
            </p>
            <div class="mt-progress">
              <div class="mt-progress__meta">
                <span>Today’s goal</span>
                <span>72%</span>
              </div>
              <div class="mt-progress__track">
                <span class="mt-progress__fill" style="--p: 72%; --c: #6366f1"></span>
              </div>
            </div>
            <div class="mt-chips">
              <span class="mt-chip">Pomodoro</span>
              <span class="mt-chip">No notifications</span>
              <span class="mt-chip">2h left</span>
            </div>
          </div>

          <div class="morph-tabs__content" data-content data-id="shared">
            <div class="mt-top">
              <span class="morph-tabs__badge" style="--accent: #10b981"
                >Team</span
              >
              <div class="mt-avatars" aria-hidden="true">
                <span style="--bg: #c7d2fe">A</span>
                <span style="--bg: #bbf7d0">M</span>
                <span style="--bg: #fde68a">J</span>
                <span class="mt-avatars__more">+3</span>
              </div>
            </div>
            <h2 class="morph-tabs__title">Shared</h2>
            <p class="morph-tabs__detail">
              Notes, links, and ideas that are still taking shape.
            </p>
            <ul class="mt-feed">
              <li>
                <span class="mt-feed__dot" style="--c: #10b981"></span>
                <span><strong>Maya</strong> dropped design tokens</span>
                <time>2m</time>
              </li>
              <li>
                <span class="mt-feed__dot" style="--c: #6366f1"></span>
                <span><strong>Alex</strong> linked the brief</span>
                <time>14m</time>
              </li>
            </ul>
          </div>

          <div class="morph-tabs__content" data-content data-id="archive">
            <div class="mt-top">
              <span class="morph-tabs__badge" style="--accent: #8b5cf6"
                >History</span
              >
              <span class="mt-count">12 items</span>
            </div>
            <h2 class="morph-tabs__title">Archive</h2>
            <p class="morph-tabs__detail">
              Past work stays close without getting in the way.
            </p>
            <ul class="mt-list">
              <li>
                <span class="mt-list__icon" style="--c: #8b5cf6">01</span>
                <div>
                  <strong>Launch checklist</strong>
                  <span>Closed · Mar 2</span>
                </div>
              </li>
              <li>
                <span class="mt-list__icon" style="--c: #a78bfa">02</span>
                <div>
                  <strong>Brand exploration</strong>
                  <span>Closed · Feb 18</span>
                </div>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </main>

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

## Source — styles.css
````css
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  min-height: 100%;
}

body {
  font-family: Inter, system-ui, -apple-system, sans-serif;
  background: #0c0c0e;
  color: #fff;
  -webkit-font-smoothing: antialiased;
}

.page {
  min-height: 100vh;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 40px 16px 80px;
  background:
    radial-gradient(ellipse 70% 50% at 50% 0%, rgba(99, 102, 241, 0.12), transparent 55%),
    #0c0c0e;
}

.morph-tabs {
  --surface: #f4f4f5;
  --shell: #18181b;
  --ink: #18181b;
  --tab-w: 176px;
  --tab-h: 56px;
  --tab-top: 24px;
  --rail-h: 80px;
  --inset: 16px;
  --gap: 16px;
  position: relative;
  isolation: isolate;
  width: min(100%, 720px);
  overflow: hidden;
  border-radius: 28px;
  background: var(--shell);
  border: 1px solid rgba(255, 255, 255, 0.06);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
  color: #fff;
  user-select: none;
  touch-action: pan-y;
}

.morph-tabs__surface {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 108px;
  z-index: 0;
  pointer-events: none;
  color: var(--surface);
  display: block;
  overflow: visible;
  transform: translateZ(0);
  backface-visibility: hidden;
  contain: strict;
}

.morph-tabs__surface path {
  transform: translateZ(0);
}

.morph-tabs__rail {
  position: relative;
  z-index: 30;
  height: var(--rail-h);
  display: block;
}

.morph-tabs__tab {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: stretch;
  cursor: grab;
  will-change: transform;
  transform: translate3d(var(--x, 16px), 0, 0);
}

.morph-tabs__tab.is-active {
  z-index: 20;
}

.morph-tabs__tab.is-dragging {
  z-index: 40;
  cursor: grabbing;
}

.morph-tabs__tab-box {
  position: relative;
  width: var(--tab-w);
  height: var(--tab-h);
  margin-top: var(--tab-top);
  display: flex;
  align-items: stretch;
}

.morph-tabs__hover {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 8px;
  border-radius: 18px;
  background: transparent;
  pointer-events: none;
  transition: background-color 0.2s ease;
}

.morph-tabs__tab:not(.is-active):hover .morph-tabs__hover {
  background: rgba(255, 255, 255, 0.06);
}

.morph-tabs__tab.is-dragging:not(.is-active) .morph-tabs__hover {
  background: #27272a;
}

.morph-tabs__btn {
  position: relative;
  z-index: 10;
  display: flex;
  align-items: center;
  width: 100%;
  height: 100%;
  min-width: 0;
  padding: 0 36px 8px 16px;
  border: 0;
  border-radius: 22px 22px 0 0;
  background: transparent;
  color: rgba(255, 255, 255, 0.55);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1;
  text-align: left;
  cursor: inherit;
  outline: none;
  transition: color 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

.morph-tabs__tab:not(.is-active):hover .morph-tabs__btn {
  color: #fff;
}

.morph-tabs__tab.is-active .morph-tabs__btn {
  color: var(--ink);
  padding-bottom: 0;
}

.morph-tabs__label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.morph-tabs__close {
  position: absolute;
  right: 8px;
  top: calc(50% - 4px);
  z-index: 20;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: rgba(255, 255, 255, 0.35);
  cursor: pointer;
  transform: translateY(-50%);
  transition:
    background-color 0.2s ease,
    color 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    top 0.45s cubic-bezier(0.22, 1, 0.36, 1);
  outline: none;
}

.morph-tabs__tab.is-active .morph-tabs__close {
  top: 50%;
  color: #a1a1aa;
}

.morph-tabs__close:hover {
  background: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.9);
}

.morph-tabs__tab.is-active .morph-tabs__close:hover {
  background: rgba(0, 0, 0, 0.06);
  color: #52525b;
}

.morph-tabs__close svg {
  width: 12px;
  height: 12px;
  display: block;
}

.morph-tabs__panel {
  position: relative;
  z-index: 20;
  margin: 0 var(--inset) var(--inset);
  min-height: 280px;
  height: 300px;
  overflow: hidden;
  border-radius: 24px;
  background: var(--surface);
  color: var(--ink);
  contain: layout style;
}

.morph-tabs__content {
  position: absolute;
  inset: 0;
  padding: 32px 36px 28px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  will-change: opacity, transform;
  backface-visibility: hidden;
  transform: translateZ(0);
}

.morph-tabs__content.is-active {
  position: absolute;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  z-index: 1;
}

.mt-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.morph-tabs__badge {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 10px;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--accent, #52525b);
  background: color-mix(in srgb, var(--accent, #52525b) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent, #52525b) 18%, transparent);
}

.mt-live {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.72rem;
  font-weight: 500;
  color: #71717a;
}

.mt-live i {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: #22c55e;
  box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
}

.mt-count {
  font-size: 0.72rem;
  font-weight: 500;
  color: #a1a1aa;
}

.morph-tabs__title {
  margin-top: 12px;
  font-size: clamp(1.65rem, 3.2vw, 2.1rem);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1.15;
  color: #09090b;
}

.morph-tabs__detail {
  margin-top: 8px;
  max-width: 22rem;
  font-size: 0.9rem;
  line-height: 1.5;
  color: #71717a;
}

.mt-progress {
  margin-top: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.mt-progress__meta {
  display: flex;
  justify-content: space-between;
  font-size: 0.75rem;
  font-weight: 500;
  color: #71717a;
}

.mt-progress__meta span:last-child {
  font-weight: 600;
  color: #3f3f46;
  font-variant-numeric: tabular-nums;
}

.mt-progress__track {
  height: 8px;
  border-radius: 999px;
  background: rgba(24, 24, 27, 0.08);
  overflow: hidden;
}

.mt-progress__fill {
  display: block;
  height: 100%;
  width: var(--p, 50%);
  border-radius: inherit;
  background: linear-gradient(
    90deg,
    var(--c, #6366f1),
    color-mix(in srgb, var(--c, #6366f1) 65%, #fff)
  );
}

.mt-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 16px;
}

.mt-chip {
  font-size: 0.72rem;
  font-weight: 500;
  padding: 6px 10px;
  border-radius: 999px;
  background: #fff;
  border: 1px solid rgba(24, 24, 27, 0.08);
  color: #52525b;
  box-shadow: 0 1px 2px rgba(24, 24, 27, 0.04);
}

.mt-avatars {
  display: flex;
  align-items: center;
}

.mt-avatars span {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  margin-right: -8px;
  border-radius: 999px;
  border: 2px solid var(--surface);
  background: var(--bg, #e4e4e7);
  font-size: 0.62rem;
  font-weight: 700;
  color: #27272a;
}

.mt-avatars__more {
  margin-right: 0 !important;
  background: #18181b !important;
  color: #fff !important;
}

.mt-feed {
  list-style: none;
  margin-top: 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.mt-feed li {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 12px;
  background: #fff;
  border: 1px solid rgba(24, 24, 27, 0.06);
  font-size: 0.8rem;
  color: #52525b;
}

.mt-feed strong {
  color: #18181b;
  font-weight: 600;
}

.mt-feed time {
  font-size: 0.7rem;
  color: #a1a1aa;
  font-variant-numeric: tabular-nums;
}

.mt-feed__dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--c, #10b981);
  flex-shrink: 0;
}

.mt-list {
  list-style: none;
  margin-top: 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.mt-list li {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 12px;
  background: #fff;
  border: 1px solid rgba(24, 24, 27, 0.06);
}

.mt-list__icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 10px;
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--c, #8b5cf6);
  background: color-mix(in srgb, var(--c, #8b5cf6) 12%, transparent);
  flex-shrink: 0;
}

.mt-list div {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.mt-list strong {
  font-size: 0.84rem;
  font-weight: 600;
  color: #18181b;
}

.mt-list span {
  font-size: 0.72rem;
  color: #a1a1aa;
}

.morph-tabs.is-empty .morph-tabs__panel {
  display: grid;
  place-items: center;
  min-height: 180px;
  color: #a1a1aa;
  font-size: 0.875rem;
}

.morph-tabs.is-empty .morph-tabs__panel::after {
  content: "No tabs open";
}

@media (max-width: 640px) {
  .morph-tabs {
    --tab-w: 140px;
    --gap: 12px;
    --inset: 12px;
  }

  .morph-tabs__content {
    padding: 24px 20px 22px;
  }

  .morph-tabs__panel {
    height: 320px;
    min-height: 300px;
  }

  .morph-tabs__btn {
    padding-left: 12px;
    padding-right: 32px;
    font-size: 0.875rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .morph-tabs__hover,
  .morph-tabs__btn,
  .morph-tabs__close {
    transition: none;
  }
}
````

## Source — script.js
````javascript
(() => {
  const DRAG_THRESHOLD = 5;
  const TAB_WIDTH = 176;
  const TAB_HEIGHT = 56;
  const TAB_RADIUS = 24;
  const RAIL_HEIGHT = 80;
  const SURFACE_INSET = 16;
  const LIQUID_JOIN = 24;
  const PANEL_RADIUS = 28;
  const SPRING = { stiffness: 340, damping: 34, mass: 0.75 };
  const SURFACE_GLIDE = {
    min: 0.58,
    max: 0.9,
    ease: "expo.out",
  };
  const CONTENT_OUT = { duration: 0.32, ease: "power2.out" };
  const CONTENT_IN = { duration: 0.5, ease: "power3.out" };

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

  function liquidTabPath(tabLeft, surfaceWidth, tabWidth, inset = SURFACE_INSET) {
    const panelLeft = inset;
    const panelRight = surfaceWidth - inset;
    const left = Math.max(panelLeft, Math.min(panelRight - tabWidth, tabLeft));
    const right = left + tabWidth;
    const top = RAIL_HEIGHT - TAB_HEIGHT;
    const bottom = RAIL_HEIGHT;
    const leftJoin = Math.max(panelLeft, left - LIQUID_JOIN);
    const rightJoin = Math.min(panelRight, right + LIQUID_JOIN);
    const leftDepth = Math.min(LIQUID_JOIN, left - leftJoin);
    const rightDepth = Math.min(LIQUID_JOIN, rightJoin - right);
    const leftControl = leftDepth * 0.55;
    const rightControl = rightDepth * 0.55;
    const leftPanelRadius = Math.min(PANEL_RADIUS, leftJoin - panelLeft);
    const rightPanelRadius = Math.min(PANEL_RADIUS, panelRight - rightJoin);

    return [
      `M${panelLeft} ${bottom + PANEL_RADIUS}`,
      `V${bottom + leftPanelRadius}`,
      `Q${panelLeft} ${bottom} ${panelLeft + leftPanelRadius} ${bottom}`,
      `H${leftJoin}`,
      `C${leftJoin + leftControl} ${bottom} ${left} ${bottom - leftDepth + leftControl} ${left} ${bottom - leftDepth}`,
      `V${top + TAB_RADIUS}`,
      `Q${left} ${top} ${left + TAB_RADIUS} ${top}`,
      `H${right - TAB_RADIUS}`,
      `Q${right} ${top} ${right} ${top + TAB_RADIUS}`,
      `V${bottom - rightDepth}`,
      `C${right} ${bottom - rightDepth + rightControl} ${rightJoin - rightControl} ${bottom} ${rightJoin} ${bottom}`,
      `H${panelRight - rightPanelRadius}`,
      `Q${panelRight} ${bottom} ${panelRight} ${bottom + rightPanelRadius}`,
      `V${bottom + PANEL_RADIUS}`,
      "Z",
    ].join(" ");
  }

  function moveItem(order, from, to) {
    if (from === to) return order.slice();
    const next = order.slice();
    const [item] = next.splice(from, 1);
    next.splice(to, 0, item);
    return next;
  }

  function createSpring(get, set, onFrame) {
    let velocity = 0;
    let target = get();
    let raf = 0;
    let running = false;
    let lastTime = 0;
    let resolveDone = null;

    function finishResolve() {
      if (resolveDone) {
        const done = resolveDone;
        resolveDone = null;
        done();
      }
    }

    function stop(resetVelocity) {
      if (raf) cancelAnimationFrame(raf);
      raf = 0;
      running = false;
      lastTime = 0;
      if (resetVelocity) velocity = 0;
      finishResolve();
    }

    function tick(now) {
      if (!lastTime) lastTime = now;
      let dt = (now - lastTime) / 1000;
      lastTime = now;
      if (dt > 0.064) dt = 0.064;
      if (dt < 0.001) dt = 0.001;

      const steps = Math.max(1, Math.ceil(dt / 0.008));
      const stepDt = dt / steps;
      let current = get();

      for (let i = 0; i < steps; i++) {
        const force = -SPRING.stiffness * (current - target);
        const damp = -SPRING.damping * velocity;
        const accel = (force + damp) / SPRING.mass;
        velocity += accel * stepDt;
        current += velocity * stepDt;
      }

      if (Math.abs(current - target) < 0.08 && Math.abs(velocity) < 4) {
        set(target);
        onFrame(target);
        running = false;
        raf = 0;
        lastTime = 0;
        velocity = 0;
        finishResolve();
        return;
      }

      set(current);
      onFrame(current);
      raf = requestAnimationFrame(tick);
    }

    function to(value, instant) {
      target = value;
      if (instant || reduceMotion.matches) {
        stop(true);
        set(value);
        onFrame(value);
        return Promise.resolve();
      }
      if (Math.abs(get() - value) < 0.05 && Math.abs(velocity) < 1) {
        set(value);
        onFrame(value);
        return Promise.resolve();
      }
      if (!running) {
        running = true;
        lastTime = 0;
        raf = requestAnimationFrame(tick);
      }
      return new Promise((resolve) => {
        resolveDone = resolve;
      });
    }

    function jump(value) {
      stop(true);
      target = value;
      set(value);
      onFrame(value);
    }

    return { to, jump, stop: () => stop(true), getTarget: () => target };
  }

  function init(root) {
    const rail = root.querySelector("[data-rail]");
    const surface = root.querySelector("[data-surface]");
    const path = root.querySelector("[data-surface-path]");
    const panel = root.querySelector("[data-panel]");
    if (!rail || !surface || !path || !panel) return;

    let tabWidth = TAB_WIDTH;
    let tabGap = 16;
    let inset = SURFACE_INSET;
    let surfaceWidth = 0;
    let order = [];
    let activeId = root.dataset.active || null;
    let draggingId = null;
    let dragTargetIndex = -1;
    let drag = null;

    const pos = {};
    const surfaceLeft = { v: SURFACE_INSET };
    const dragLeft = { v: SURFACE_INSET };

    function paint(left) {
      if (surfaceWidth <= inset * 2 || !activeId) {
        path.setAttribute("d", "");
        return;
      }
      path.setAttribute(
        "d",
        liquidTabPath(left, surfaceWidth, tabWidth, inset)
      );
    }

    let surfaceTween = null;
    let engineReady = false;
    let warming = false;

    function glideSurfaceTo(left, instant) {
      if (surfaceTween) {
        surfaceTween.kill();
        surfaceTween = null;
      }

      const from = Number(surfaceLeft.v);

      if (instant || reduceMotion.matches) {
        surfaceLeft.v = left;
        paint(left);
        return;
      }

      const dist = Math.abs(from - left);
      if (dist < 0.5) {
        surfaceLeft.v = left;
        paint(left);
        return;
      }

      const duration = Math.min(
        SURFACE_GLIDE.max,
        Math.max(SURFACE_GLIDE.min, 0.48 + dist / 520)
      );

      surfaceLeft.v = from;
      paint(from);

      surfaceTween = gsap.fromTo(
        surfaceLeft,
        { v: from },
        {
          v: left,
          duration,
          ease: SURFACE_GLIDE.ease,
          overwrite: true,
          immediateRender: false,
          onStart: () => paint(from),
          onUpdate: () => paint(surfaceLeft.v),
          onComplete: () => {
            surfaceLeft.v = left;
            paint(left);
            surfaceTween = null;
          },
        }
      );
    }

    function warmEngine() {
      if (engineReady || warming || reduceMotion.matches) return;
      warming = true;
      measure();

      const w = Math.max(surfaceWidth, 1);
      const base = activeId ? slotLeft(order.indexOf(activeId)) : inset;
      const offscreen = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "path"
      );

      for (let i = 0; i < order.length; i++) {
        const d = liquidTabPath(slotLeft(i), w, tabWidth, inset);
        offscreen.setAttribute("d", d);
      }
      for (let t = 0; t <= 1.001; t += 0.2) {
        const x = inset + t * (tabWidth + tabGap);
        offscreen.setAttribute("d", liquidTabPath(x, w, tabWidth, inset));
      }

      surfaceLeft.v = base;
      paint(base);

      panel.querySelectorAll("[data-content]").forEach((el) => {
        const on = el.dataset.id === activeId;
        gsap.set(el, {
          opacity: on ? 1 : 0,
          y: 0,
          force3D: true,
        });
      });
      gsap.set([surface, path, root], { force3D: true });

      const probe = { v: 0 };
      gsap.to(probe, {
        v: 1,
        duration: 0.2,
        ease: SURFACE_GLIDE.ease,
        onUpdate: () => {
          offscreen.setAttribute(
            "d",
            liquidTabPath(
              base + probe.v * 40,
              w,
              tabWidth,
              inset
            )
          );
        },
        onComplete: () => {
          surfaceLeft.v = base;
          paint(base);
          warming = false;
          engineReady = true;
        },
      });
    }

    const tabSprings = {};

    function ensureTabSpring(id) {
      if (tabSprings[id]) return tabSprings[id];
      if (!pos[id]) pos[id] = { v: inset };
      tabSprings[id] = createSpring(
        () => pos[id].v,
        (v) => {
          pos[id].v = v;
        },
        (v) => {
          const el = getTab(id);
          if (el) el.style.setProperty("--x", `${v}px`);
        }
      );
      return tabSprings[id];
    }

    function getTab(id) {
      return rail.querySelector(`[data-tab][data-id="${id}"]`);
    }

    function getContent(id) {
      return panel.querySelector(`[data-content][data-id="${id}"]`);
    }

    function getTabs() {
      return order.map(getTab).filter(Boolean);
    }

    function measure() {
      surfaceWidth = root.clientWidth;
      const styles = getComputedStyle(root);
      const gap = parseFloat(styles.getPropertyValue("--gap"));
      const w = parseFloat(styles.getPropertyValue("--tab-w"));
      const insetVal = parseFloat(styles.getPropertyValue("--inset"));
      if (Number.isFinite(gap)) tabGap = gap;
      if (Number.isFinite(w)) tabWidth = w;
      if (Number.isFinite(insetVal)) inset = insetVal;
      const box = rail.querySelector(".morph-tabs__tab-box");
      if (box?.offsetWidth) tabWidth = box.offsetWidth;

      const vbW = Math.max(surfaceWidth, 1);
      const vbH = RAIL_HEIGHT + PANEL_RADIUS;
      surface.setAttribute("viewBox", `0 0 ${vbW} ${vbH}`);
      surface.setAttribute("width", String(vbW));
      surface.setAttribute("height", String(vbH));
    }

    function slotLeft(i) {
      return inset + i * (tabWidth + tabGap);
    }

    function visualIndex(index) {
      const start = draggingId ? order.indexOf(draggingId) : -1;
      if (start < 0 || dragTargetIndex < 0) return index;
      if (index === start) return dragTargetIndex;
      if (dragTargetIndex > start && index > start && index <= dragTargetIndex) {
        return index - 1;
      }
      if (dragTargetIndex < start && index >= dragTargetIndex && index < start) {
        return index + 1;
      }
      return index;
    }

    function layoutTabs(instant) {
      order.forEach((id, index) => {
        if (id === draggingId) return;
        const left = slotLeft(visualIndex(index));
        const spring = ensureTabSpring(id);
        const el = getTab(id);
        if (!el) return;
        if (instant || reduceMotion.matches) {
          spring.jump(left);
        } else {
          spring.to(left);
        }
      });
    }

    function syncActive() {
      getTabs().forEach((tab) => {
        const id = tab.dataset.id;
        const on = id === activeId;
        tab.classList.toggle("is-active", on);
        const btn = tab.querySelector(".morph-tabs__btn");
        if (btn) {
          btn.setAttribute("aria-selected", on ? "true" : "false");
          btn.tabIndex = on ? 0 : -1;
        }
      });
      if (activeId) {
        panel.setAttribute("aria-labelledby", `tab-${activeId}`);
      }
    }

    function moveSurfaceToActive(instant) {
      if (!activeId || draggingId === activeId) return;
      const idx = order.indexOf(activeId);
      if (idx < 0) return;
      glideSurfaceTo(slotLeft(visualIndex(idx)), instant);
    }

    function setActive(id) {
      if (!id || !order.includes(id) || id === activeId) return;

      const prev = activeId;
      const targetLeft = slotLeft(order.indexOf(id));
      const fromLeft = surfaceLeft.v;

      activeId = id;
      root.dataset.active = id;

      paint(fromLeft);
      glideSurfaceTo(targetLeft, false);
      syncActive();

      requestAnimationFrame(() => {
        swapContent(prev, id);
      });
    }

    function swapContent(fromId, toId) {
      const from = fromId ? getContent(fromId) : null;
      const to = toId ? getContent(toId) : null;
      if (from === to) return;

      if (from) gsap.killTweensOf(from);
      if (to) gsap.killTweensOf(to);

      panel.querySelectorAll("[data-content]").forEach((el) => {
        if (el !== from && el !== to) {
          gsap.killTweensOf(el);
          el.classList.remove("is-active");
          el.style.visibility = "hidden";
          el.style.pointerEvents = "none";
          gsap.set(el, { opacity: 0, y: 0, clearProps: "transform" });
        }
      });

      if (from) {
        from.classList.remove("is-active");
        from.style.pointerEvents = "none";
        if (reduceMotion.matches) {
          gsap.set(from, { opacity: 0, y: 0 });
          from.style.visibility = "hidden";
        } else {
          gsap.fromTo(
            from,
            { opacity: 1, y: 0 },
            {
              opacity: 0,
              y: -8,
              duration: CONTENT_OUT.duration,
              ease: CONTENT_OUT.ease,
              overwrite: true,
              force3D: true,
              onComplete: () => {
                from.style.visibility = "hidden";
                gsap.set(from, { y: 0 });
              },
            }
          );
        }
      }

      if (to) {
        to.classList.add("is-active");
        to.style.visibility = "visible";
        to.style.pointerEvents = "auto";
        if (reduceMotion.matches) {
          gsap.set(to, { opacity: 1, y: 0 });
        } else {
          gsap.fromTo(
            to,
            { opacity: 0, y: 14 },
            {
              opacity: 1,
              y: 0,
              duration: CONTENT_IN.duration,
              ease: CONTENT_IN.ease,
              delay: from ? 0.04 : 0,
              overwrite: true,
              force3D: true,
            }
          );
        }
      }
    }

    function closeTab(id) {
      const index = order.indexOf(id);
      if (index < 0) return;
      const wasActive = activeId === id;
      const nextOrder = order.filter((x) => x !== id);
      const nextActive = wasActive
        ? nextOrder[Math.min(index, nextOrder.length - 1)] || null
        : activeId;

      order = nextOrder;
      if (tabSprings[id]) {
        tabSprings[id].stop();
        delete tabSprings[id];
      }
      delete pos[id];

      const tab = getTab(id);
      const content = getContent(id);
      if (tab) {
        gsap.to(tab, {
          opacity: 0,
          scale: 0.94,
          duration: reduceMotion.matches ? 0 : 0.18,
          onComplete: () => tab.remove(),
        });
      }
      if (content) content.remove();

      if (!order.length) {
        activeId = null;
        root.dataset.active = "";
        root.classList.add("is-empty");
        paint(0);
        return;
      }

      if (wasActive) {
        activeId = null;
        setActive(nextActive);
      } else {
        layoutTabs(false);
        moveSurfaceToActive(false);
      }
    }

    function onPointerDown(id, e) {
      if (e.button !== 0 || drag) return;
      if (e.target.closest("[data-close]")) return;
      const startIndex = order.indexOf(id);
      if (startIndex < 0) return;

      const slots = order.map((_, i) => slotLeft(i));
      const startLeft = pos[id]?.v ?? slots[startIndex];

      drag = {
        id,
        pointerId: e.pointerId,
        originX: e.clientX,
        startLeft,
        startIndex,
        targetIndex: startIndex,
        moved: false,
        finishing: false,
        startOrder: order.slice(),
        slots,
        el: getTab(id),
      };
      dragLeft.v = startLeft;
    }

    function onPointerMove(e) {
      if (!drag || drag.finishing || e.pointerId !== drag.pointerId) return;
      const delta = e.clientX - drag.originX;
      if (!drag.moved && Math.abs(delta) < DRAG_THRESHOLD) return;
      e.preventDefault();

      if (!drag.moved) {
        drag.moved = true;
        try {
          drag.el?.setPointerCapture(e.pointerId);
        } catch (_) {}
        draggingId = drag.id;
        dragTargetIndex = drag.startIndex;
        drag.el?.classList.add("is-dragging");
        if (drag.id === activeId) {
          if (surfaceTween) {
            surfaceTween.kill();
            surfaceTween = null;
          }
          surfaceLeft.v = drag.startLeft;
          paint(drag.startLeft);
        }
      }

      const minL = drag.slots[0];
      const maxL = drag.slots[drag.slots.length - 1];
      const visual = Math.max(minL, Math.min(maxL, drag.startLeft + delta));

      let target = drag.startIndex;
      if (visual >= drag.startLeft) {
        for (let i = drag.startIndex + 1; i < drag.slots.length; i++) {
          if (visual + tabWidth / 2 >= drag.slots[i]) target = i;
        }
      } else {
        for (let i = drag.startIndex - 1; i >= 0; i--) {
          if (visual <= drag.slots[i] + tabWidth / 2) target = i;
        }
      }

      dragLeft.v = visual;
      if (pos[drag.id]) pos[drag.id].v = visual;
      if (drag.el) drag.el.style.setProperty("--x", `${visual}px`);

      if (drag.id === activeId) {
        surfaceLeft.v = visual;
        paint(visual);
      }

      if (target !== drag.targetIndex) {
        drag.targetIndex = target;
        dragTargetIndex = target;
        layoutTabs(false);
      }
    }

    function onPointerUp(pointerId) {
      if (!drag || drag.pointerId !== pointerId || drag.finishing) return;

      if (!drag.moved) {
        drag = null;
        return;
      }

      drag.finishing = true;
      const session = drag;
      const targetLeft = session.slots[session.targetIndex];

      const finish = () => {
        const next = moveItem(
          session.startOrder,
          session.startIndex,
          session.targetIndex
        );
        order = next;

        if (session.id === activeId) {
          surfaceLeft.v = targetLeft;
          paint(targetLeft);
        } else if (activeId && pos[activeId]) {
          surfaceLeft.v = pos[activeId].v;
          paint(pos[activeId].v);
        }

        if (pos[session.id]) pos[session.id].v = targetLeft;
        if (session.el) {
          session.el.style.setProperty("--x", `${targetLeft}px`);
          session.el.classList.remove("is-dragging");
        }

        ensureTabSpring(session.id).jump(targetLeft);
        draggingId = null;
        dragTargetIndex = -1;
        drag = null;
        layoutTabs(true);
        moveSurfaceToActive(true);
      };

      if (reduceMotion.matches) {
        finish();
        return;
      }

      const state = { v: dragLeft.v };
      gsap.to(state, {
        v: targetLeft,
        duration: 0.55,
        ease: "power3.out",
        onUpdate: () => {
          dragLeft.v = state.v;
          if (pos[session.id]) pos[session.id].v = state.v;
          if (session.el) session.el.style.setProperty("--x", `${state.v}px`);
          if (session.id === activeId) {
            surfaceLeft.v = state.v;
            paint(state.v);
          }
        },
        onComplete: finish,
      });
    }

    function bindTab(tab) {
      const id = tab.dataset.id;
      tab.addEventListener("pointerdown", (e) => onPointerDown(id, e));
      tab.addEventListener("pointermove", onPointerMove);
      tab.addEventListener("pointerup", (e) => onPointerUp(e.pointerId));
      tab.addEventListener("pointercancel", (e) => onPointerUp(e.pointerId));
      tab.addEventListener("lostpointercapture", (e) => onPointerUp(e.pointerId));

      const btn = tab.querySelector(".morph-tabs__btn");
      btn?.addEventListener("click", () => {
        if (drag?.id === id && drag.moved) return;
        setActive(id);
      });

      btn?.addEventListener("keydown", (e) => {
        const index = order.indexOf(id);
        if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
          e.preventDefault();
          const dir = e.key === "ArrowLeft" ? -1 : 1;
          const next = (index + dir + order.length) % order.length;
          setActive(order[next]);
          getTab(order[next])?.querySelector(".morph-tabs__btn")?.focus();
        }
      });

      const close = tab.querySelector("[data-close]");
      close?.addEventListener("pointerdown", (e) => e.stopPropagation());
      close?.addEventListener("click", (e) => {
        e.stopPropagation();
        closeTab(id);
      });
    }

    function boot() {
      const tabs = Array.from(rail.querySelectorAll("[data-tab]"));
      order = tabs.map((t) => t.dataset.id);
      if (!activeId || !order.includes(activeId)) activeId = order[0] || null;
      root.dataset.active = activeId || "";

      tabs.forEach(bindTab);
      measure();

      order.forEach((id, i) => {
        pos[id] = { v: slotLeft(i) };
        const el = getTab(id);
        if (el) el.style.setProperty("--x", `${pos[id].v}px`);
        ensureTabSpring(id).jump(pos[id].v);
      });

      panel.querySelectorAll("[data-content]").forEach((el) => {
        const on = el.dataset.id === activeId;
        el.classList.toggle("is-active", on);
        el.style.visibility = on ? "visible" : "hidden";
        if (!on) gsap.set(el, { opacity: 0 });
      });

      syncActive();
      if (activeId) {
        surfaceLeft.v = slotLeft(order.indexOf(activeId));
        paint(surfaceLeft.v);
      }

      const settleLayout = () => {
        measure();
        order.forEach((id, i) => {
          const left = slotLeft(i);
          pos[id].v = left;
          getTab(id)?.style.setProperty("--x", `${left}px`);
        });
        if (activeId) {
          surfaceLeft.v = slotLeft(order.indexOf(activeId));
          paint(surfaceLeft.v);
        }
      };

      requestAnimationFrame(() => {
        settleLayout();
        requestAnimationFrame(() => {
          settleLayout();
          warmEngine();
          if (typeof requestIdleCallback === "function") {
            requestIdleCallback(() => warmEngine(), { timeout: 400 });
          } else {
            window.setTimeout(warmEngine, 120);
          }
        });
      });

      new ResizeObserver(() => {
        measure();
        order.forEach((id, i) => {
          if (id === draggingId) return;
          const left = slotLeft(visualIndex(i));
          ensureTabSpring(id).jump(left);
        });
        if (activeId && !draggingId) {
          surfaceLeft.v = slotLeft(order.indexOf(activeId));
          paint(surfaceLeft.v);
        }
      }).observe(root);

      window.addEventListener("pointerup", (e) => onPointerUp(e.pointerId), true);
      window.addEventListener(
        "pointercancel",
        (e) => onPointerUp(e.pointerId),
        true
      );
    }

    boot();
  }

  function start() {
    document.querySelectorAll("[data-morph-tabs]").forEach(init);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start);
  } else {
    start();
  }
})();
````
