type SlideMeta = { status: string; title: string };

interface TimelineSlide {
  position: number;
  element?: HTMLElement;
  [key: string]: any;
}

type ProgressData = { progress: number; position: number };

const SPACE_BETWEEN_SLIDER = 100;
const SPACE_SLIDER_DURATION = 1000;
const MAX_SPEED = 20;
const SLOWDOWN_RANGE = 200;
const MAX_CHANGE_SPEED = 1.2;
const FRAME_DURATION = 1000 / 60;

const displayVariables = (data: SlideMeta[], slides: SlideMeta[]): void => {
  const variablesElement = document.querySelector("#params");
  const slidesElement = document.querySelector("#slides");
  if (!variablesElement || !slidesElement) return;

  variablesElement.innerHTML = data
    .map(
      ({ status, title }) => `
        <div class="timeline3d__variable">
          <span>${title}</span>
          <span>${status}</span>
        </div>`,
    )
    .join("");

  if (slides) {
    slidesElement.innerHTML = slides
      .map(
        ({ status, title }) => `
          <div class="timeline3d__variable-small">
            <span>${title}</span>
            <span>${status}</span>
          </div>`,
      )
      .join("");
  }
};

const renderSliders = (data: TimelineSlide[]): string =>
  data
    .map(
      (slide) => `
        <div class="slide" data-position="${slide.position}"></div>
      `,
    )
    .join("");

export const runScript = (): void => {
  const container = document.getElementById("3dTimeline") as HTMLElement | null;
  if (!container) return;

  const raw = container.dataset.slider3d;
  const sliderData: TimelineSlide[] = raw
    ? (JSON.parse(raw) as TimelineSlide[])
    : [];
  if (!sliderData.length) return;

  const slidesPerView = Math.round(
    SPACE_SLIDER_DURATION / SPACE_BETWEEN_SLIDER,
  );
  const maxPosition =
    Math.max(...sliderData.map((item) => item.position)) + slidesPerView + 1;
  const byPos = new Map<number, TimelineSlide>(
    sliderData.map((card) => [card.position, card]),
  );

  const slides: TimelineSlide[] = Array.from(
    { length: maxPosition + 1 },
    (_, pos) => byPos.get(pos) ?? { position: pos },
  );

  container.innerHTML = `
    <div class="timeline3d">
      <div class="timeline3d__category"></div>
      <div class="timeline3d__slider">
        ${renderSliders(slides)}
      </div>
      <div class="scrollbar" id="timeline3d-scrollbar">
        <div class="scrollbar__thumb" id="timeline3d-scrollbar-thumb"></div>
      </div>
      <div class="timeline3d__variables" id="params"></div>
      <div class="timeline3d__variables" id="slides"></div>
    </div>
  `;

  slides.forEach((slide) => {
    slide.element =
      container.querySelector<HTMLElement>(
        `.slide[data-position="${slide.position}"]`,
      ) ?? undefined;
  });

  const track = container.querySelector<HTMLDivElement>(
    "#timeline3d-scrollbar",
  )!;
  const thumb = container.querySelector<HTMLDivElement>(
    "#timeline3d-scrollbar-thumb",
  )!;
  const root = container.querySelector<HTMLDivElement>(".timeline3d")!;

  const clamp = (val: number, min: number, max: number): number =>
    Math.min(Math.max(val, min), max);

  let thumbX = 0;
  let startX = 0;
  let startOffset = 0;
  let activeId: number | null = null;
  let targetStatus = 0;
  const sliderWidth = sliderData.length * SPACE_BETWEEN_SLIDER;
  let sliderScrollStatus = 0;
  let status = 0;
  let speed = 0;
  let isAnimated = false;
  let lastFrameTime = 0;
  let activePosition = 0;
  let endPosition = 0;
  let visibleSlides: ProgressData[] = [];
  let firstPositionCard: TimelineSlide | undefined;

  const startAnimated = (): void => {
    if (!isAnimated) {
      isAnimated = true;
      lastFrameTime = 0;
      requestAnimationFrame(animate);
    }
  };

  const renderCards = (cardsProgress: ProgressData[]): void => {
    if (!cardsProgress.length) return;

    if (!firstPositionCard) {
      firstPositionCard = slides[cardsProgress[0].position];
      firstPositionCard.element?.classList.add("slide__first");
    } else if (firstPositionCard.position !== cardsProgress[0].position) {
      firstPositionCard.element?.classList.remove("slide__first");
      firstPositionCard = slides[cardsProgress[0].position];
      firstPositionCard.element?.classList.add("slide__first");
    }

    cardsProgress.forEach(({ progress, position }) => {
      const card = slides[position];
      card.element?.style.setProperty("--progress", progress.toString());
    });
  };

  const render = (): void => {
    const maxOffset = track.clientWidth - thumb.clientWidth;
    sliderScrollStatus = maxOffset ? (thumbX / maxOffset) * 100 : 0;
    targetStatus = (sliderWidth / 100) * sliderScrollStatus;

    const distance = targetStatus - status;
    const absDistance = Math.abs(distance);

    let desiredSpeed = 0;
    if (absDistance > 0) {
      desiredSpeed = Math.sign(distance) * MAX_SPEED;
      if (absDistance < SLOWDOWN_RANGE) {
        desiredSpeed *= absDistance / SLOWDOWN_RANGE;
      }
    }

    const deltaSpeed = clamp(
      desiredSpeed - speed,
      -MAX_CHANGE_SPEED,
      MAX_CHANGE_SPEED,
    );
    speed += deltaSpeed;
    status = clamp(status + speed, 0, sliderWidth);

    sliderScrollStatus = (status / sliderWidth) * 100;
    root.style.setProperty(
      "--thumb-x",
      `${(sliderScrollStatus / 100) * maxOffset}px`,
    );

    if (absDistance < 0.5 && Math.abs(speed) < 0.05) {
      status = targetStatus;
      speed = 0;
    }

    activePosition = Math.round(status / SPACE_BETWEEN_SLIDER);
    endPosition =
      activePosition + Math.round(SPACE_SLIDER_DURATION / SPACE_BETWEEN_SLIDER);

    visibleSlides = [];

    for (let i = activePosition; i <= endPosition; i += 1) {
      const start = (i - slidesPerView) * SPACE_BETWEEN_SLIDER;
      const rawProgress = (status - start) / SPACE_SLIDER_DURATION;
      if (rawProgress > 1 || rawProgress < 0) continue;
      const progress = Math.round(rawProgress * 1000) / 1000;
      visibleSlides.push({ progress, position: i });
    }

    renderCards(visibleSlides);

    root.setAttribute("data-count", `${visibleSlides.length}`);

    const result: SlideMeta[] = visibleSlides.map(({ progress, position }) => ({
      status: `${Math.round(progress * 100)}%`,
      title: `slide ${position}`,
    }));

    displayVariables(
      [
        { title: "targetStatus", status: `${targetStatus.toFixed(2)}px` },
        { title: "realStatus", status: `${status.toFixed(2)}px` },
        { title: "speed", status: `${speed.toFixed(2)}px` },
        {
          title: "sliderScrollStatus",
          status: `${sliderScrollStatus.toFixed(2)}%`,
        },
        { title: "activePosition", status: `${activePosition}` },
        { title: "endPosition", status: `${endPosition}` },
        { title: "maxPosition", status: `${maxPosition}` },
      ],
      result,
    );
  };

  const animate = (time: number): void => {
    if (time - lastFrameTime >= FRAME_DURATION) {
      lastFrameTime = time;
      render();
    }

    if (Math.abs(targetStatus - status) > 0.01 || Math.abs(speed) > 0.01) {
      requestAnimationFrame(animate);
    } else {
      speed = 0;
      isAnimated = false;
    }
  };

  const onPointerMove = (e: PointerEvent): void => {
    const dx = e.clientX - startX;
    const maxOffset = track.clientWidth - thumb.clientWidth;
    thumbX = clamp(startOffset + dx, 0, maxOffset);
    startAnimated();
  };

  const onPointerUp = (): void => {
    document.removeEventListener("pointermove", onPointerMove);
    document.removeEventListener("pointerup", onPointerUp);
    if (activeId !== null) {
      thumb.releasePointerCapture(activeId);
      activeId = null;
    }
  };

  thumb.addEventListener("pointerdown", (e: PointerEvent): void => {
    activeId = e.pointerId;
    thumb.setPointerCapture(activeId);
    startX = e.clientX;
    startOffset = thumbX;
    document.addEventListener("pointermove", onPointerMove);
    document.addEventListener("pointerup", onPointerUp);
  });

  startAnimated();
};
3D TIMELINE
Теперь нужно убрать хвосты рендера, можно просто проходиться по всем слайдерам, однако можно сделать оптимизированней, через первый слайд вешать display block через ~ на след слайды на все остальный display none