# Morphing Select

> A select control that morphs open into a stacked option list with elastic motion — refined form UI beyond native dropdowns.

**Definition:** Morphing Select is a components 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: Components
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-02
- Canonical page: https://html.bosconvertpro.com/collection/morphing-select
- Full-page preview: https://html.bosconvertpro.com/collection/morphing-select/full
- Live demo: https://html.bosconvertpro.com/demos/morphing-select/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/morphing-select/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Morphing Select?

Morphing Select is a components UI animation from collections.html. A select control that morphs open into a stacked option list with elastic motion — refined form UI beyond native dropdowns.

### What is Morphing Select built with?

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

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

### Is Morphing Select free to use?

Yes. The Morphing Select 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 Select</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="morph-select">
      <div class="morph-select__field">
        <span class="morph-select__label" id="morph-select-label">Category</span>

        <div
          class="morph-select__stack"
          data-morph-select
          data-duration="0.8"
          data-ease="elastic.out(1,0.82)"
          data-stagger="0.02"
          data-gap="12"
          data-direction="down"
        >
          <div class="morph-select__goo" data-morph-select-goo>
            <svg
              class="morph-select__filter"
              width="0"
              height="0"
              aria-hidden="true"
              focusable="false"
            >
              <defs>
                <filter
                  id="morph-select-goo"
                  x="-50%"
                  y="-700%"
                  width="200%"
                  height="1500%"
                  color-interpolation-filters="sRGB"
                >
                  <feGaussianBlur
                    in="SourceGraphic"
                    stdDeviation="9"
                    result="blur"
                  />
                  <feColorMatrix
                    in="blur"
                    mode="matrix"
                    values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 20 -10"
                    result="goo"
                  />
                  <feComposite
                    in="SourceGraphic"
                    in2="goo"
                    operator="atop"
                  />
                </filter>
              </defs>
            </svg>
            <div class="morph-select__trigger-shape" data-morph-select-trigger-shape></div>
            <div class="morph-select__panel-shape" data-morph-select-panel-shape></div>
          </div>

          <div class="morph-select__trigger-surface" aria-hidden="true"></div>

          <div class="morph-select__content">
            <button
              class="morph-select__trigger"
              type="button"
              data-morph-select-trigger
              aria-haspopup="listbox"
              aria-expanded="false"
              aria-labelledby="morph-select-label"
            >
              <div class="morph-select__value" data-morph-select-value>
                <span class="morph-select__value-text">All categories</span>
                <span
                  class="morph-select__value-text morph-select__value-text--next"
                  aria-hidden="true"
                  >All categories</span
                >
              </div>
              <svg
                class="morph-select__caret"
                data-morph-select-caret
                viewBox="0 0 16 16"
                fill="none"
                aria-hidden="true"
              >
                <path
                  d="M4 6l4 4 4-4"
                  stroke="currentColor"
                  stroke-width="1.5"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </button>

            <div class="morph-select__panel" data-morph-select-panel>
              <div
                class="morph-select__viewport"
                data-morph-select-viewport
              >
                <div
                  class="morph-select__highlight"
                  data-morph-select-highlight
                  aria-hidden="true"
                ></div>
                <ul
                  class="morph-select__list"
                  data-morph-select-list
                  role="listbox"
                  tabindex="-1"
                  aria-labelledby="morph-select-label"
                >
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-0"
                    role="option"
                    data-morph-select-option
                    data-value="all"
                    data-label="All categories"
                    aria-selected="true"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">All categories</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-1"
                    role="option"
                    data-morph-select-option
                    data-value="design"
                    data-label="Design"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Design</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-2"
                    role="option"
                    data-morph-select-option
                    data-value="engineering"
                    data-label="Engineering"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Engineering</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-3"
                    role="option"
                    data-morph-select-option
                    data-value="product"
                    data-label="Product"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Product</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-4"
                    role="option"
                    data-morph-select-option
                    data-value="marketing"
                    data-label="Marketing"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Marketing</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-5"
                    role="option"
                    data-morph-select-option
                    data-value="sales"
                    data-label="Sales"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Sales</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-6"
                    role="option"
                    data-morph-select-option
                    data-value="operations"
                    data-label="Operations"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Operations</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-7"
                    role="option"
                    data-morph-select-option
                    data-value="finance"
                    data-label="Finance"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Finance</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-8"
                    role="option"
                    data-morph-select-option
                    data-value="legal"
                    data-label="Legal"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Legal</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-9"
                    role="option"
                    data-morph-select-option
                    data-value="support"
                    data-label="Support"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Support</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-10"
                    role="option"
                    data-morph-select-option
                    data-value="research"
                    data-label="Research"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Research</span>
                    </span>
                  </li>
                  <li
                    class="morph-select__option"
                    id="morph-select-opt-11"
                    role="option"
                    data-morph-select-option
                    data-value="content"
                    data-label="Content"
                    aria-selected="false"
                  >
                    <span class="morph-select__option-label">
                      <span class="morph-select__option-text">Content</span>
                    </span>
                  </li>
                </ul>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

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

## Source — styles.css
````css
@font-face {
  font-family: "Switzer";
  src: url("/fonts/Switzer-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

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

body {
  font-family: "Switzer", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", sans-serif;
  background: #f0f0f0;
  color: #0a0a0a;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.morph-select {
  --morph-select-bg: #ebebe9;
  --morph-select-ink: #101010;
  --morph-select-fg: rgba(255, 250, 242, 0.92);
  --morph-select-fg-soft: rgba(255, 255, 255, 0.78);
  --morph-select-fg-dim: rgba(255, 255, 255, 0.55);
  --morph-select-margin: 40px;
  --morph-select-gap: 12px;
  --morph-select-list-max-height: 252px;
  --morph-select-font: "Switzer", -apple-system, BlinkMacSystemFont, sans-serif;

  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 100vh;
  background-color: var(--morph-select-bg);
  font-family: var(--morph-select-font);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

.morph-select__field {
  position: relative;
  width: 280px;
}

.morph-select__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.morph-select__stack {
  position: relative;
}

.morph-select__goo {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.morph-select__filter {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

.morph-select__trigger-shape,
.morph-select__panel-shape {
  position: absolute;
  top: 0;
  left: 0;
  background-color: var(--morph-select-ink);
  transform-origin: 50% 0%;
  will-change: transform, width, height;
}

.morph-select__trigger-shape {
  width: 100%;
  height: 100%;
  border-radius: 999px;
}

.morph-select__trigger-surface {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  background-color: var(--morph-select-ink);
  pointer-events: none;
}

.morph-select__panel-shape {
  border-radius: 16px;
}

.morph-select__content {
  position: relative;
  z-index: 1;
}

.morph-select__trigger {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  height: 52px;
  padding: 0 20px;
  border: none;
  background: transparent;
  color: var(--morph-select-fg);
  cursor: pointer;
  font-family: inherit;
  outline: none;
}

.morph-select__trigger:focus-visible {
  outline: 2px solid #fd551d;
  outline-offset: 2px;
  border-radius: 999px;
}

.morph-select__value {
  position: relative;
  overflow-x: visible;
  overflow-y: clip;
  height: 1.3em;
  line-height: 1.3em;
}

.morph-select__value-text {
  display: block;
  font-size: 15px;
  font-weight: 380;
  letter-spacing: -0.01em;
  white-space: nowrap;
  transform: translateY(0%);
}

.morph-select__value-text--next {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.morph-select__caret {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  display: block;
  transform-origin: 50% 50%;
}

.morph-select__panel {
  position: absolute;
  top: calc(100% + var(--morph-select-gap));
  left: 0;
  width: 100%;
  z-index: 2;
}

[data-morph-select][data-direction="up"] .morph-select__panel {
  top: auto;
  bottom: calc(100% + var(--morph-select-gap));
}

[data-morph-select][data-direction="up"] .morph-select__panel-shape {
  top: auto;
  bottom: 0;
}

.morph-select__viewport {
  position: relative;
  max-height: var(--morph-select-list-max-height);
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.22) transparent;
}

.morph-select__viewport::-webkit-scrollbar {
  width: 6px;
}

.morph-select__viewport::-webkit-scrollbar-track {
  background: transparent;
  margin: 8px 0;
}

.morph-select__viewport::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.22);
  border-radius: 100vw;
}

.morph-select__viewport::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.35);
}

.morph-select__list {
  position: relative;
  z-index: 1;
  padding: 6px;
  list-style: none;
  margin: 0;
  outline: none;
}

.morph-select__highlight {
  position: absolute;
  left: 6px;
  right: 6px;
  top: 0;
  border-radius: 10px;
  background-color: rgba(255, 255, 255, 0.1);
  pointer-events: none;
  z-index: 0;
}

.morph-select__option {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  height: 40px;
  padding: 0 14px;
  border-radius: 10px;
  cursor: pointer;
}

.morph-select__option-label {
  position: relative;
  display: block;
}

.morph-select__option-text {
  display: block;
  font-size: 14px;
  font-weight: 380;
  letter-spacing: -0.01em;
  color: var(--morph-select-fg-soft);
  white-space: nowrap;
  opacity: 0;
  padding-left: 0;
  transition: padding-left 0.28s cubic-bezier(0.16, 1, 0.3, 1);
}

.morph-select__option:hover .morph-select__option-text,
.morph-select__option.is-active .morph-select__option-text {
  padding-left: 6px;
}

.morph-select__option[aria-selected="true"] .morph-select__option-text {
  color: var(--morph-select-fg);
}

@media (max-width: 479px) {
  .morph-select {
    --morph-select-margin: 20px;
  }

  .morph-select__field {
    width: calc(100% - var(--morph-select-margin) * 2);
    max-width: 280px;
  }
}
````

## Source — script.js
````javascript
(function () {
  "use strict";

  let instanceId = 0;

  function whenReady(callback, attempt) {
    attempt = attempt || 0;
    if (typeof gsap === "undefined" || typeof CustomEase === "undefined") {
      if (attempt < 100) {
        setTimeout(function () {
          whenReady(callback, attempt + 1);
        }, 50);
      } else {
        console.error("[Morphing Select] GSAP / CustomEase failed to load");
      }
      return;
    }
    callback();
  }

  function isDisabledByMedia(root) {
    const raw = root.dataset.disable;
    if (!raw) return false;

    const map = {
      mobile: "(max-width: 479px)",
      tablet: "(max-width: 991px)",
      landscape: "(orientation: landscape) and (max-width: 767px)",
      desktop: "(min-width: 992px)",
    };

    return raw.split(",").some(function (token) {
      const query = map[token.trim()];
      return query && window.matchMedia(query).matches;
    });
  }

  function initSelect(root) {
    const uid = "morph-select-" + ++instanceId;

    const trigger = root.querySelector("[data-morph-select-trigger]");
    const triggerShape = root.querySelector("[data-morph-select-trigger-shape]");
    const panelShape = root.querySelector("[data-morph-select-panel-shape]");
    const goo = root.querySelector("[data-morph-select-goo]");
    const filterSvg = root.querySelector(".morph-select__filter");
    const caret = root.querySelector("[data-morph-select-caret]");
    const valueEl = root.querySelector("[data-morph-select-value]");
    const valueText = valueEl
      ? valueEl.querySelector(".morph-select__value-text")
      : null;
    const valueTextNext = valueEl
      ? valueEl.querySelector(".morph-select__value-text--next")
      : null;
    const panel = root.querySelector("[data-morph-select-panel]");
    const viewport = root.querySelector("[data-morph-select-viewport]");
    const list = root.querySelector("[data-morph-select-list]");
    const highlight = root.querySelector("[data-morph-select-highlight]");
    const options = Array.prototype.slice.call(
      root.querySelectorAll("[data-morph-select-option]")
    );

    if (
      !(
        trigger &&
        triggerShape &&
        panelShape &&
        panel &&
        viewport &&
        list &&
        options.length
      )
    ) {
      return null;
    }

    if (isDisabledByMedia(root)) return null;

    const filterId = "morph-select-goo-" + uid;
    const filterEl = filterSvg && filterSvg.querySelector("filter");
    const blurEl = filterSvg && filterSvg.querySelector("feGaussianBlur");
    if (filterEl) filterEl.setAttribute("id", filterId);

    const duration = parseFloat(root.dataset.duration || "0.8");
    const ease = root.dataset.ease || "elastic.out(1,0.82)";
    const stagger = parseFloat(root.dataset.stagger || "0.02");
    const gap = parseFloat(root.dataset.gap || "12");
    const direction = root.dataset.direction === "up" ? "up" : "down";

    root.style.setProperty("--morph-select-gap", gap + "px");

    const closeEase = ease.indexOf("elastic") === 0 ? "morphSelectEase" : ease;

    let isOpen = false;
    let timeline = null;
    const optionReady = options.map(function () {
      return false;
    });

    let selectedIndex = options.findIndex(function (opt) {
      return opt.getAttribute("aria-selected") === "true";
    });
    if (selectedIndex < 0) selectedIndex = 0;

    let highlightIndex = -1;
    const gooState = { std: 9 };

    function setGooActive(active) {
      goo.style.filter = active ? "url(#" + filterId + ")" : "none";
    }

    function syncBlur() {
      if (blurEl) blurEl.setAttribute("stdDeviation", String(gooState.std));
    }

    function panelSize() {
      return { w: panel.offsetWidth, h: panel.offsetHeight };
    }

    function resetPanelShapeToPill() {
      const w = trigger.offsetWidth;
      const h = trigger.offsetHeight;
      gsap.set(panelShape, {
        width: w,
        height: h,
        y: 0,
        borderRadius: h / 2,
        force3D: true,
      });
    }

    function hideOptionText() {
      options.forEach(function (opt, i) {
        optionReady[i] = false;
        const text = opt.querySelector(".morph-select__option-text");
        if (text) {
          gsap.killTweensOf(text);
          gsap.set(text, { opacity: 0 });
        }
      });
    }

    function setExpanded(expanded) {
      trigger.setAttribute("aria-expanded", String(expanded));
      panel.setAttribute("aria-hidden", String(!expanded));
      if (expanded) {
        panel.removeAttribute("inert");
      } else {
        panel.setAttribute("inert", "");
      }
    }

    function rotateCaret(open) {
      gsap.to(caret, {
        rotation: open ? 180 : 0,
        duration: 0.4,
        ease: "morphSelectInOut",
        force3D: true,
        overwrite: "auto",
      });
    }

    function getOptionOffset(opt) {
      return (
        opt.getBoundingClientRect().top -
        viewport.getBoundingClientRect().top +
        viewport.scrollTop
      );
    }

    function scrollOptionIntoView(opt) {
      const viewTop = viewport.scrollTop;
      const viewBottom = viewTop + viewport.clientHeight;
      const optTop = getOptionOffset(opt);
      const optBottom = optTop + opt.offsetHeight;

      if (optTop < viewTop) {
        viewport.scrollTop = optTop - 6;
      } else if (optBottom > viewBottom) {
        viewport.scrollTop = optBottom - viewport.clientHeight + 6;
      }
    }

    function setActiveOption(index) {
      options.forEach(function (opt, i) {
        opt.classList.toggle("is-active", i === index);
      });
    }

    function clearActiveOption() {
      options.forEach(function (opt) {
        opt.classList.remove("is-active");
      });
    }

    function moveHighlight(index, animate) {
      if (!highlight || index < 0) return;

      const opt = options[index];
      const same = index === highlightIndex;
      const first = highlightIndex < 0;
      highlightIndex = index;

      setActiveOption(index);
      scrollOptionIntoView(opt);

      const y = getOptionOffset(opt);
      const h = opt.offsetHeight;

      if (same && animate !== false) {
        gsap.set(highlight, { y: y, height: h });
        return;
      }

      if (first) {
        gsap.set(highlight, {
          y: y,
          height: h,
          autoAlpha: 0,
          force3D: true,
        });
        gsap.to(highlight, {
          autoAlpha: 1,
          duration: 0.18,
          ease: "power2.out",
        });
        return;
      }

      gsap.to(highlight, {
        y: y,
        height: h,
        autoAlpha: 1,
        duration: animate === false ? 0 : 0.22,
        ease: "morphSelectInOut",
        force3D: true,
        overwrite: "auto",
      });
    }

    function hideHighlight() {
      if (!highlight || highlightIndex < 0) return;
      highlightIndex = -1;
      clearActiveOption();
      gsap.to(highlight, {
        autoAlpha: 0,
        duration: 0.2,
        ease: "power2.in",
        overwrite: "auto",
      });
    }

    function openPanel() {
      if (isOpen) return;
      isOpen = true;
      setExpanded(true);
      trigger.setAttribute(
        "aria-activedescendant",
        options[selectedIndex].id
      );

      viewport.scrollTop = 0;
      scrollOptionIntoView(options[selectedIndex]);

      const size = panelSize();
      const wasActive = timeline && timeline.isActive();

      if (timeline) timeline.kill();
      hideOptionText();
      hideHighlight();

      if (!wasActive) resetPanelShapeToPill();

      gooState.std = 9;
      syncBlur();
      setGooActive(true);

      timeline = gsap.timeline({ defaults: { force3D: true } });
      rotateCaret(true);

      const yOffset =
        (trigger.offsetHeight + gap) * (direction === "up" ? -1 : 1);

      timeline
        .to(
          panelShape,
          {
            y: yOffset,
            duration: duration,
            ease: ease,
          },
          0
        )
        .to(
          panelShape,
          {
            width: size.w,
            height: size.h,
            borderRadius: 16,
            duration: duration,
            ease: ease,
          },
          0.07
        );

      timeline.to(
        gooState,
        {
          std: 0,
          duration: 0.25 * duration,
          ease: "power1.in",
          onUpdate: syncBlur,
          onComplete: function () {
            setGooActive(false);
          },
        },
        0.4 * duration
      );

      timeline.to(
        panel,
        {
          autoAlpha: 1,
          duration: 0.2,
          ease: "power2.out",
        },
        0.3 * duration
      );

      const textStart = 0.32 * duration;
      options.forEach(function (opt, i) {
        const text = opt.querySelector(".morph-select__option-text");
        if (!text) return;
        timeline.to(
          text,
          {
            opacity: 1,
            duration: 0.3,
            ease: "power2.out",
            onComplete: function () {
              optionReady[i] = true;
              if (opt.matches(":hover")) moveHighlight(i);
            },
          },
          textStart + i * stagger
        );
      });

      timeline.call(
        function () {
          moveHighlight(selectedIndex, false);
        },
        null,
        textStart + options.length * stagger
      );
    }

    function closePanel(opts) {
      if (!isOpen) return;
      isOpen = false;
      setExpanded(false);
      trigger.removeAttribute("aria-activedescendant");

      const restoreFocus = !opts || opts.restoreFocus !== false;

      if (timeline) timeline.kill();
      hideHighlight();

      gooState.std = 0;
      syncBlur();
      setGooActive(true);

      timeline = gsap.timeline({
        defaults: { force3D: true },
        onComplete: function () {
          setGooActive(false);
        },
      });

      timeline.to(
        gooState,
        {
          std: 9,
          duration: 0.15,
          ease: "power2.out",
          onUpdate: syncBlur,
        },
        0
      );

      rotateCaret(false);

      timeline.to(
        panel,
        {
          autoAlpha: 0,
          duration: 0.2,
          ease: "power2.in",
        },
        0
      );

      const closeDur = 0.65 * duration;
      timeline
        .to(
          panelShape,
          {
            width: trigger.offsetWidth,
            height: trigger.offsetHeight,
            borderRadius: trigger.offsetHeight / 2,
            duration: closeDur,
            ease: closeEase,
          },
          0
        )
        .to(
          panelShape,
          {
            y: 0,
            duration: closeDur,
            ease: closeEase,
          },
          0.07
        );

      timeline.to(
        gooState,
        {
          std: 0,
          duration: 0.3 * closeDur,
          ease: "power1.in",
          onUpdate: syncBlur,
        },
        0.55 * closeDur
      );

      timeline.call(hideOptionText, null, 0.22);

      if (restoreFocus) trigger.focus();
    }

    function toggle() {
      if (isOpen) closePanel();
      else openPanel();
    }

    function selectOption(index) {
      if (index < 0 || index >= options.length) return;

      const opt = options[index];
      selectedIndex = index;

      options.forEach(function (item, i) {
        item.setAttribute("aria-selected", String(i === index));
      });

      const label = opt.dataset.label || opt.textContent.trim();

      if (valueText && valueTextNext) {
        gsap.killTweensOf([valueText, valueTextNext]);
        gsap.set(valueText, { yPercent: 0, y: 0 });
        valueTextNext.textContent = label;
        gsap.set(valueTextNext, { yPercent: 100, y: 0 });

        gsap
          .timeline({ defaults: { force3D: true } })
          .to(
            valueText,
            {
              yPercent: -100,
              y: 0,
              duration: 0.4,
              ease: "morphSelectInOut",
            },
            0
          )
          .to(
            valueTextNext,
            {
              yPercent: 0,
              y: 0,
              duration: 0.4,
              ease: "morphSelectInOut",
            },
            0
          )
          .call(function () {
            valueText.textContent = label;
            gsap.set(valueText, { yPercent: 0, y: 0 });
            gsap.set(valueTextNext, { yPercent: 100, y: 0 });
          });
      }

      root.dispatchEvent(
        new CustomEvent("morph-select-change", {
          detail: {
            value: opt.dataset.value || "",
            label: opt.dataset.label || opt.textContent.trim(),
            index: index,
          },
        })
      );

      closePanel();
    }

    function setActiveDescendant(index) {
      moveHighlight(index);
      trigger.setAttribute("aria-activedescendant", options[index].id);
    }

    function moveActiveBy(delta) {
      const len = options.length;
      const base = highlightIndex >= 0 ? highlightIndex : selectedIndex;
      setActiveDescendant((base + delta + len) % len);
    }

    function onDocKeydown(e) {
      if (e.key === "Escape" && isOpen) closePanel();
    }

    function onDocPointerDown(e) {
      if (isOpen && !root.contains(e.target)) {
        closePanel({ restoreFocus: false });
      }
    }

    trigger.addEventListener("click", toggle);

    trigger.addEventListener("keydown", function (e) {
      if (isOpen) {
        if (e.key === "ArrowDown") {
          e.preventDefault();
          moveActiveBy(1);
        } else if (e.key === "ArrowUp") {
          e.preventDefault();
          moveActiveBy(-1);
        } else if (e.key === "Home") {
          e.preventDefault();
          setActiveDescendant(0);
        } else if (e.key === "End") {
          e.preventDefault();
          setActiveDescendant(options.length - 1);
        } else if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          selectOption(highlightIndex >= 0 ? highlightIndex : selectedIndex);
        }
      } else if (
        e.key === "Enter" ||
        e.key === " " ||
        e.key === "ArrowDown" ||
        e.key === "ArrowUp"
      ) {
        e.preventDefault();
        openPanel();
      }
    });

    options.forEach(function (opt, i) {
      opt.addEventListener("pointerenter", function () {
        if (optionReady[i]) moveHighlight(i);
      });
      opt.addEventListener("click", function () {
        selectOption(i);
      });
    });

    viewport.addEventListener("mouseleave", function () {
      if (isOpen) moveHighlight(selectedIndex);
    });

    viewport.addEventListener("scroll", function () {
      if (!isOpen || highlightIndex < 0) return;
      const opt = options[highlightIndex];
      if (!opt) return;
      gsap.set(highlight, {
        y: getOptionOffset(opt),
        height: opt.offsetHeight,
      });
    });

    document.addEventListener("keydown", onDocKeydown);
    document.addEventListener("pointerdown", onDocPointerDown);

    let resizeTimer = null;
    function onResize() {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(function () {
        if (isOpen) {
          const size = panelSize();
          gsap.set(panelShape, {
            width: size.w,
            height: size.h,
            y:
              (trigger.offsetHeight + gap) *
              (direction === "up" ? -1 : 1),
          });
          moveHighlight(
            highlightIndex >= 0 ? highlightIndex : selectedIndex,
            false
          );
        } else {
          resetPanelShapeToPill();
        }
      }, 150);
    }

    function onVisibility() {
      if (document.hidden) gsap.globalTimeline.pause();
      else gsap.globalTimeline.resume();
    }

    window.addEventListener("resize", onResize);
    document.addEventListener("visibilitychange", onVisibility);

    setExpanded(false);
    setGooActive(false);
    hideOptionText();
    gsap.set(panel, { autoAlpha: 0 });
    resetPanelShapeToPill();
    syncBlur();

    options.forEach(function (opt, i) {
      opt.id = "morph-select-opt-" + uid + "-" + i;
      opt.setAttribute("aria-selected", String(i === selectedIndex));
    });

    if (valueText) {
      valueText.textContent =
        options[selectedIndex].dataset.label ||
        options[selectedIndex].textContent.trim();
    }

    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(function () {
        if (!isOpen) resetPanelShapeToPill();
      });
    }

    gsap.matchMedia().add("(prefers-reduced-motion: reduce)", function () {
      gsap.globalTimeline.timeScale(20);
    });

    return {
      open: openPanel,
      close: closePanel,
      toggle: toggle,
      select: selectOption,
      kill: function () {
        window.removeEventListener("resize", onResize);
        document.removeEventListener("visibilitychange", onVisibility);
        document.removeEventListener("keydown", onDocKeydown);
        document.removeEventListener("pointerdown", onDocPointerDown);
        if (timeline) timeline.kill();
      },
    };
  }

  whenReady(function () {
    gsap.registerPlugin(CustomEase);

    if (!CustomEase.get("morphSelectEase")) {
      CustomEase.create("morphSelectEase", "M0,0 C0.3,0.9 0.1,1 1,1");
    }
    if (!CustomEase.get("morphSelectInOut")) {
      CustomEase.create("morphSelectInOut", "M0,0 C0.7,0 0.16,1 1,1");
    }

    const instances = [];

    document.querySelectorAll("[data-morph-select]").forEach(function (el) {
      const api = initSelect(el);
      if (api) instances.push(api);
    });

    window.MorphSelect = {
      refresh: function () {
        instances.forEach(function (api) {
          api.kill();
        });
        instances.length = 0;
        document.querySelectorAll("[data-morph-select]").forEach(function (el) {
          const api = initSelect(el);
          if (api) instances.push(api);
        });
      },
      open: function () {
        instances.forEach(function (api) {
          api.open();
        });
      },
      close: function () {
        instances.forEach(function (api) {
          api.close();
        });
      },
    };
  });
})();
````
