# Radial Gallery

> Orbiting image rings that spin around the viewport with hover pop — immersive radial gallery for travel and portfolio showcases.

**Definition:** Radial Gallery is a sliders 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: Sliders
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-03
- Canonical page: https://html.bosconvertpro.com/collection/radial-gallery
- Full-page preview: https://html.bosconvertpro.com/collection/radial-gallery/full
- Live demo: https://html.bosconvertpro.com/demos/radial-gallery/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/radial-gallery/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Radial Gallery?

Radial Gallery is a sliders UI animation from collections.html. Orbiting image rings that spin around the viewport with hover pop — immersive radial gallery for travel and portfolio showcases.

### What is Radial Gallery built with?

Radial Gallery 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 Radial Gallery in my project?

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

### Is Radial Gallery free to use?

Yes. The Radial Gallery 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>Radial Gallery — GSAP</title>
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link rel="preconnect" href="https://cdn.jsdelivr.net" />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <main class="app">
      <section class="radial-gallery" aria-label="Travel destinations radial gallery">
        <!-- Left orbiting ring -->
        <div
          class="radial-gallery__ring"
          data-radial-gallery
          data-side="left"
          data-mobile="center"
          data-buffer="0.4"
          data-tile-aspect="1.7778"
          data-speed="90"
        >
          <div class="radial-gallery__orbit">
            <div class="radial-gallery__track" data-radial-track>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=480&q=80"
                  alt="Alpine mountain peaks above the clouds"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1499856871958-5b9627545d1a?auto=format&fit=crop&w=480&q=80"
                  alt="Paris skyline with Eiffel Tower at dusk"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1516483638261-f4dbaf036963?auto=format&fit=crop&w=480&q=80"
                  alt="Colorful coastal town along the Italian Riviera"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?auto=format&fit=crop&w=480&q=80"
                  alt="Venice canal with classic gondolas"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1533105079780-92b9be482077?auto=format&fit=crop&w=480&q=80"
                  alt="Santorini white buildings against the sea"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?auto=format&fit=crop&w=480&q=80"
                  alt="Calm mountain lake at sunrise"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?auto=format&fit=crop&w=480&q=80"
                  alt="Desert highway stretching toward the horizon"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=480&q=80"
                  alt="Tropical beach with turquoise water"
                  loading="eager"
                  decoding="async"
                />
              </div>
            </div>
          </div>
        </div>

        <!-- Right orbiting ring -->
        <div
          class="radial-gallery__ring"
          data-radial-gallery
          data-side="right"
          data-mobile="hide"
          data-buffer="0.4"
          data-tile-aspect="1.7778"
          data-speed="90"
        >
          <div class="radial-gallery__orbit">
            <div class="radial-gallery__track" data-radial-track>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=480&q=80"
                  alt="Tropical beach with turquoise water"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?auto=format&fit=crop&w=480&q=80"
                  alt="Desert highway stretching toward the horizon"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?auto=format&fit=crop&w=480&q=80"
                  alt="Calm mountain lake at sunrise"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1533105079780-92b9be482077?auto=format&fit=crop&w=480&q=80"
                  alt="Santorini white buildings against the sea"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=480&q=80"
                  alt="Alpine mountain peaks above the clouds"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1499856871958-5b9627545d1a?auto=format&fit=crop&w=480&q=80"
                  alt="Paris skyline with Eiffel Tower at dusk"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1516483638261-f4dbaf036963?auto=format&fit=crop&w=480&q=80"
                  alt="Colorful coastal town along the Italian Riviera"
                  loading="eager"
                  decoding="async"
                />
              </div>
              <div class="radial-gallery__tile" data-radial-tile>
                <img
                  class="radial-gallery__image"
                  src="https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?auto=format&fit=crop&w=480&q=80"
                  alt="Venice canal with classic gondolas"
                  loading="eager"
                  decoding="async"
                />
              </div>
            </div>
          </div>
        </div>

        <!-- Center call-to-action -->
        <div class="radial-gallery__content">
          <h1 class="radial-gallery__title">Where will you wander next?</h1>
          <p class="radial-gallery__subtitle">
            Curated routes, local guides, and hidden stays — inspiration for
            every kind of traveler, every week.
          </p>
          <div class="radial-gallery__actions">
            <a class="radial-gallery__button radial-gallery__button--primary" href="#">
              Start exploring
            </a>
            <a class="radial-gallery__button radial-gallery__button--ghost" href="#">
              View destinations
            </a>
          </div>
        </div>
      </section>
    </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
/* ============================================
   Radial Gallery — minimal premium
   ============================================ */

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

:root {
  --rg-bg: #0a0a0a;
  --rg-text: #f5f5f5;
  --rg-muted: #9a9a9a;
  --rg-border: rgba(255, 255, 255, 0.18);
  --rg-border-hover: rgba(255, 255, 255, 0.4);
  --rg-radius-tile: 6px;
  --rg-radius-btn: 999px;
  --rg-ease: cubic-bezier(0.16, 1, 0.3, 1);
}

html,
body {
  height: 100%;
}

body {
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
    sans-serif;
  background: var(--rg-bg);
  color: var(--rg-text);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.app {
  min-height: 100vh;
  width: 100%;
}

/* --------------------------------------------
   Section — quiet gradient only
   -------------------------------------------- */

.radial-gallery {
  position: relative;
  width: 100%;
  min-height: 100vh;
  overflow: hidden;
  background:
    radial-gradient(ellipse 80% 60% at 50% 40%, #161616 0%, transparent 70%),
    linear-gradient(180deg, #111111 0%, #0a0a0a 55%, #070707 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 96px 24px;
}

/* --------------------------------------------
   Orbit ring wrappers
   -------------------------------------------- */

.radial-gallery__ring {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

.radial-gallery__probe {
  position: absolute;
  visibility: hidden;
  height: 0;
  pointer-events: none;
}

.radial-gallery__orbit {
  position: absolute;
  top: 50%;
  width: var(--rg-diameter, min(180vw, 1040px));
  height: var(--rg-diameter, min(180vw, 1040px));
  perspective: 1600px;
}

.radial-gallery__ring[data-rg-mode="right"] .radial-gallery__orbit {
  right: 0;
  transform: translate(50%, -50%);
}

.radial-gallery__ring[data-rg-mode="left"] .radial-gallery__orbit {
  left: 0;
  transform: translate(-50%, -50%);
}

.radial-gallery__ring[data-rg-mode="center"] .radial-gallery__orbit {
  left: 50%;
  transform: translate(-50%, -50%);
}

.radial-gallery__track {
  position: relative;
  width: 100%;
  height: 100%;
}

/* --------------------------------------------
   Tiles
   -------------------------------------------- */

.radial-gallery__tile {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--rg-tile-w, 166px);
  height: var(--rg-tile-h, 94px);
  margin-left: calc(var(--rg-tile-w, 166px) / -2);
  margin-top: calc(var(--rg-tile-h, 94px) / -2);
  overflow: visible;
  cursor: pointer;
  pointer-events: auto;
  will-change: transform;
}

.radial-gallery__tile-inner {
  width: 100%;
  height: 100%;
  transform: rotate(var(--rg-tilt, 0deg))
    translateY(calc(var(--rg-out, 0px) * -1))
    rotate(calc(var(--rg-tilt, 0deg) * -1));
  will-change: transform;
}

.radial-gallery__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  border-radius: var(--rg-radius-tile);
  pointer-events: none;
  background: #1a1a1a;
}

/* --------------------------------------------
   Center CTA
   -------------------------------------------- */

.radial-gallery__content {
  position: relative;
  z-index: 2;
  max-width: 540px;
  text-align: center;
  color: var(--rg-text);
}

.radial-gallery__title {
  margin: 0 0 20px;
  font-size: clamp(34px, 6vw, 56px);
  line-height: 1.02;
  letter-spacing: -0.02em;
  font-weight: 600;
  color: var(--rg-text);
}

.radial-gallery__subtitle {
  margin: 0 auto 36px;
  max-width: 44ch;
  font-size: 17px;
  line-height: 1.5;
  color: var(--rg-muted);
}

.radial-gallery__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: center;
}

/* Buttons — color / border only, no scale */
.radial-gallery__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 48px;
  padding: 0 28px;
  border-radius: var(--rg-radius-btn);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition:
    background-color 0.35s var(--rg-ease),
    color 0.35s var(--rg-ease),
    border-color 0.35s var(--rg-ease);
}

.radial-gallery__button--primary {
  background-color: #f5f5f5;
  color: #0a0a0a;
  border-color: #f5f5f5;
}

.radial-gallery__button--primary:hover {
  background-color: #ffffff;
  border-color: #ffffff;
}

.radial-gallery__button--ghost {
  background-color: transparent;
  color: var(--rg-text);
  border-color: var(--rg-border);
}

.radial-gallery__button--ghost:hover {
  border-color: var(--rg-border-hover);
  background-color: rgba(255, 255, 255, 0.04);
}

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

@media (max-width: 640px) {
  .radial-gallery {
    padding: 72px 20px;
  }

  .radial-gallery__actions {
    flex-direction: column;
    align-items: stretch;
  }

  .radial-gallery__button {
    width: 100%;
  }
}
````

## Source — script.js
````javascript
/**
 * Radial Gallery
 * Two (or one) rings of image tiles orbit continuously around the viewport.
 * Tiles pop outward on hover. Fully data-attribute driven and resize-aware.
 *
 * Requires: GSAP 3 (loaded via CDN in index.html)
 */

(() => {
  "use strict";

  if (typeof gsap === "undefined") {
    console.error("[Radial Gallery] GSAP failed to load from CDN.");
    return;
  }

  /* ── Easing presets ── */
  const EASE_POP = "back.out(2)";
  const EASE_SETTLE = "power2.out";
  const EASE_SPEED = "power2.out";

  /* ── Defaults (overridable via data-* attributes) ── */
  const DEFAULTS = {
    side: "right", // left | center | right
    radius: "clamp(120px, calc(43.5vw - 260px), 520px)",
    cardWidth: "clamp(120px, 13vw, 190px)",
    tileAspect: 16 / 9,
    speed: 90, // seconds for one full revolution
    hoverPause: false,
    buffer: 0.4, // extra spacing factor between tiles
    mobile: "center", // hide | keep | center
    mobileBreakpoint: 768,
    mobileRadius: "clamp(220px, min(80vw, 48vh), 520px)",
    popOutPx: 32,
  };

  /**
   * Resolve a CSS length string to pixels by measuring a temporary probe.
   */
  function measureCssLength(root, cssValue) {
    const probe = document.createElement("div");
    probe.className = "radial-gallery__probe";
    probe.style.width = cssValue;
    root.appendChild(probe);
    const pixels = probe.offsetWidth;
    root.removeChild(probe);
    return pixels;
  }

  /**
   * Initialise a single radial ring root element.
   * @param {HTMLElement} root — element with [data-radial-gallery]
   * @returns {{ pause, resume, rebuild, demo } | null}
   */
  function createRadialRing(root) {
    const track = root.querySelector("[data-radial-track]");
    if (!track) return null;

    /* Collect image sources from the authored markup */
    const imageSources = Array.from(
      track.querySelectorAll("[data-radial-tile] img")
    )
      .map((img) => img.getAttribute("src"))
      .filter(Boolean);

    if (!imageSources.length) return null;

    /* Config from data attributes */
    const sideAttr = root.dataset.side;
    const side =
      sideAttr === "left" || sideAttr === "center" ? sideAttr : DEFAULTS.side;

    const radiusCss = root.dataset.radius || DEFAULTS.radius;
    const cardCss = root.dataset.card || DEFAULTS.cardWidth;
    const tileAspect =
      parseFloat(root.dataset.tileAspect) || DEFAULTS.tileAspect;
    const speed = parseFloat(root.dataset.speed) || DEFAULTS.speed;
    const hoverPause = root.dataset.hoverPause === "true";
    const bufferRaw = parseFloat(root.dataset.buffer);
    const buffer = Number.isNaN(bufferRaw)
      ? DEFAULTS.buffer
      : Math.max(0, bufferRaw);

    const mobileAttr = root.dataset.mobile;
    const mobileMode =
      mobileAttr === "hide" || mobileAttr === "keep"
        ? mobileAttr
        : DEFAULTS.mobile;

    const mobileBreakpoint =
      parseFloat(root.dataset.mobileBreakpoint) || DEFAULTS.mobileBreakpoint;
    const mobileRadiusCss = root.dataset.mobileRadius || DEFAULTS.mobileRadius;

    /* Mutable state */
    let orbitTween = null;
    let hoverCount = 0;
    let resumeTimer = null;
    let resizeTimer = null;
    let motionEnabled = false;

    let tiles = [];
    let inners = [];
    let tileStates = [];
    let tileCount = 0;
    let isCenterMode = false;
    let sideTilt = 270; // degrees: left=90, right=270, center=0

    const speedProxy = { factor: 1 };
    const orbitProxy = { orbit: 0 };

    /* ── Layout rebuild ── */

    function applyLayoutMetrics(radiusValue) {
      const radiusPx = measureCssLength(root, radiusValue);
      const tileW = Math.max(1, measureCssLength(root, cardCss));
      const tileH = tileW / tileAspect;
      const step = tileW * (1 + buffer);

      tileCount = Math.max(6, Math.round((2 * Math.PI * radiusPx) / step));

      root.style.setProperty("--rg-radius", radiusPx + "px");
      root.style.setProperty("--rg-diameter", 2 * radiusPx + "px");
      root.style.setProperty("--rg-tile-w", tileW + "px");
      root.style.setProperty("--rg-tile-h", tileH + "px");
    }

    function buildTiles() {
      track.textContent = "";
      tiles = [];
      inners = [];
      tileStates = [];

      for (let i = 0; i < tileCount; i++) {
        const tile = document.createElement("div");
        tile.className = "radial-gallery__tile";
        tile.setAttribute("data-radial-tile", "");

        const inner = document.createElement("div");
        inner.className = "radial-gallery__tile-inner";

        const img = document.createElement("img");
        img.className = "radial-gallery__image";
        img.src = imageSources[i % imageSources.length];
        img.alt = "";
        img.loading = "eager";
        img.decoding = "async";

        inner.appendChild(img);
        tile.appendChild(inner);
        bindTileHover(tile, i);
        track.appendChild(tile);

        tiles.push(tile);
        inners.push(inner);
        tileStates.push({ out: 0 });
      }
    }

    function rebuild() {
      const isMobile = window.innerWidth <= mobileBreakpoint;

      if (isMobile && mobileMode === "hide") {
        root.style.display = "none";
        track.textContent = "";
        tiles = [];
        inners = [];
        tileStates = [];
        tileCount = 0;
        return;
      }

      root.style.display = "";

      const activeSide = isMobile && mobileMode === "center" ? "center" : side;
      isCenterMode = activeSide === "center";
      sideTilt = activeSide === "left" ? 90 : 270;

      root.setAttribute("data-rg-mode", activeSide);
      root.style.setProperty(
        "--rg-tilt",
        (isCenterMode ? 0 : sideTilt) + "deg"
      );

      const radiusValue =
        isMobile && isCenterMode ? mobileRadiusCss : radiusCss;
      applyLayoutMetrics(radiusValue);
      buildTiles();
    }

    /* ── Tile placement on the circle ── */

    function tileTransform(index, orbitDegrees) {
      const angle = (index / tileCount) * 360 + orbitDegrees;
      const counter = isCenterMode ? -angle : -sideTilt;
      return (
        "rotate(" +
        angle +
        "deg) translateY(calc(var(--rg-radius) * -1)) rotate(" +
        counter +
        "deg)"
      );
    }

    function placeAllTiles(orbitDegrees) {
      for (let i = 0; i < tiles.length; i++) {
        tiles[i].style.transform = tileTransform(i, orbitDegrees);
      }
    }

    /* ── Speed control (hover pause) ── */

    function setSpeedFactor(factor) {
      gsap.to(speedProxy, {
        factor: factor,
        duration: 0.8,
        ease: EASE_SPEED,
        overwrite: true,
        onUpdate: function () {
          if (orbitTween) orbitTween.timeScale(speedProxy.factor);
        },
      });
    }

    function pauseOrbit() {
      if (orbitTween) setSpeedFactor(0);
    }

    function resumeOrbit() {
      if (orbitTween) setSpeedFactor(1);
    }

    /* ── Hover pop-out ── */

    function setTilePop(index, isOut) {
      gsap.to(tileStates[index], {
        out: isOut ? DEFAULTS.popOutPx : 0,
        duration: isOut ? 0.28 : 0.24,
        ease: isOut ? EASE_POP : EASE_SETTLE,
        overwrite: true,
        onUpdate: function () {
          if (inners[index]) {
            inners[index].style.setProperty(
              "--rg-out",
              tileStates[index].out + "px"
            );
          }
        },
      });
    }

    function bindTileHover(tile, index) {
      tile.addEventListener("mouseenter", function () {
        if (!motionEnabled) return;
        setTilePop(index, true);
        if (hoverPause) {
          hoverCount += 1;
          if (resumeTimer) {
            clearTimeout(resumeTimer);
            resumeTimer = null;
          }
          pauseOrbit();
        }
      });

      tile.addEventListener("mouseleave", function () {
        if (!motionEnabled) return;
        setTilePop(index, false);
        if (hoverPause) {
          hoverCount = Math.max(0, hoverCount - 1);
          if (hoverCount === 0) {
            resumeTimer = setTimeout(function () {
              resumeOrbit();
              resumeTimer = null;
            }, 80);
          }
        }
      });
    }

    /* ── Resize ── */

    function onResize() {
      rebuild();
      placeAllTiles(orbitProxy.orbit);
    }

    window.addEventListener("resize", function () {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(onResize, 150);
    });

    document.addEventListener("visibilitychange", function () {
      if (document.hidden) {
        gsap.globalTimeline.pause();
      } else {
        gsap.globalTimeline.resume();
      }
    });

    /* ── Initial layout ── */

    rebuild();
    placeAllTiles(0);

    /* ── Continuous orbit (respects reduced motion) ── */

    gsap.matchMedia().add("(prefers-reduced-motion: no-preference)", function () {
      motionEnabled = true;
      orbitTween = gsap.to(orbitProxy, {
        orbit: 360,
        duration: speed,
        ease: "none",
        repeat: -1,
        onUpdate: function () {
          placeAllTiles(orbitProxy.orbit);
        },
      });

      return function cleanup() {
        motionEnabled = false;
        if (orbitTween) {
          orbitTween.kill();
          orbitTween = null;
        }
        orbitProxy.orbit = 0;
        placeAllTiles(0);
      };
    });

    /* ── Demo timeline (hover preview of one card) ── */

    const demoProxy = { out: 0 };
    let demoIndex = -1;

    function applyDemoPop() {
      if (demoIndex >= 0 && inners[demoIndex]) {
        inners[demoIndex].style.setProperty(
          "--rg-out",
          demoProxy.out + "px"
        );
      }
    }

    let demoTimeline = gsap.timeline({ paused: true });

    function rebuildDemoTimeline() {
      if (demoTimeline) demoTimeline.kill();
      demoTimeline = gsap.timeline({ paused: true });

      demoTimeline.call(
        function () {
          const relative =
            (((sideTilt - orbitProxy.orbit) % 360) + 360) % 360;
          demoIndex =
            tileCount > 0
              ? Math.round((relative / 360) * tileCount) % tileCount
              : -1;
        },
        null,
        0
      );

      if (hoverPause) {
        demoTimeline.to(
          speedProxy,
          {
            factor: 0,
            duration: 0.8,
            ease: EASE_SPEED,
            onUpdate: function () {
              if (orbitTween) orbitTween.timeScale(speedProxy.factor);
            },
            data: { label: "Ring eases to a halt" },
          },
          0.01
        );
      }

      demoTimeline.to(
        demoProxy,
        {
          out: DEFAULTS.popOutPx,
          duration: 0.28,
          ease: EASE_POP,
          onUpdate: applyDemoPop,
          data: { label: "Card slides out of the ring" },
        },
        0.01
      );

      demoTimeline.to(demoProxy, {
        out: "+=0",
        duration: 0.8,
        ease: "none",
        data: { label: "Holds clear of the ring" },
      });

      demoTimeline.to(demoProxy, {
        out: 0,
        duration: 0.24,
        ease: EASE_SETTLE,
        onUpdate: applyDemoPop,
        data: { label: "Card settles back into the ring" },
      });

      if (hoverPause) {
        demoTimeline.to(speedProxy, {
          factor: 1,
          duration: 0.8,
          ease: EASE_SPEED,
          onUpdate: function () {
            if (orbitTween) orbitTween.timeScale(speedProxy.factor);
          },
          data: { label: "Ring picks the pace back up" },
        });
      }
    }

    rebuildDemoTimeline();

    return {
      pause: pauseOrbit,
      resume: resumeOrbit,
      rebuild: onResize,
      demo: function () {
        if (!demoTimeline || !tiles.length) return null;
        rebuildDemoTimeline();
        demoTimeline.restart(true, false);
        return demoTimeline;
      },
    };
  }

  /* ── Boot all rings on the page ── */

  function init() {
    const instances = [];

    document
      .querySelectorAll("[data-radial-gallery]")
      .forEach(function (root) {
        const instance = createRadialRing(root);
        if (instance) instances.push(instance);
      });

    window.RadialGallery = {
      refresh: function () {
        instances.forEach(function (inst) {
          inst.rebuild();
        });
      },
      pause: function () {
        instances.forEach(function (inst) {
          inst.pause();
        });
      },
      resume: function () {
        instances.forEach(function (inst) {
          inst.resume();
        });
      },
      demo: function () {
        return instances[0] ? instances[0].demo() : null;
      },
    };
  }

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