I have this timeline.js file for a site that shows a timelapse of files that are saved every minute on several cameras. there's an issue where if I scrub along the timeline, it's fine. but if i hold the mouse still, the images fade out. I want the image to stay static when not moving along the timeline.; document.addEventListener("DOMContentLoaded", () => {
  const dateInput = document.getElementById("dateInput");
  const cameraSelect = document.getElementById("cameraSelect");
  const hourLabels = document.getElementById("hourLabels");
  const timelineWrapper = document.getElementById("timelineWrapper");
  const timeSlider = document.getElementById("timeSlider");
  const previewGrid = document.getElementById("previewGrid");
  const zoomInBtn = document.getElementById("zoomIn");
  const zoomOutBtn = document.getElementById("zoomOut");
  const timeIndicator = document.getElementById("timeIndicator");

  // Add persistent header and image container
let header = document.createElement("h3");
let imageContainer = document.createElement("div");
imageContainer.className = "previewCrossfade";
previewGrid.appendChild(header);
previewGrid.appendChild(imageContainer);

// Create two image layers
const currentImg = document.createElement("img");
const incomingImg = document.createElement("img");

currentImg.className = "previewLayer";
incomingImg.className = "previewLayer top";

imageContainer.appendChild(currentImg);
imageContainer.appendChild(incomingImg);

  let zoomLevel = 3;
  let selectedCameras = new Set();
  let timelineData = [];
  const imageCache = new Set();
  let lastTimestamp = null;
  let lastHoverTime = 0;
  const debounceDelay = 50;

  function buildTimeMarkers() {
    hourLabels.innerHTML = "";
    const step = zoomLevel >= 10 ? 5 : zoomLevel >= 6 ? 10 : 60;
    for (let i = 0; i <= 1440; i += step) {
      const hh = String(Math.floor(i / 60)).padStart(2, "0");
      const mm = String(i % 60).padStart(2, "0");
      const label = document.createElement("div");
      label.className = "timeTick";
      label.textContent = `${hh}:${mm}`;
      label.style.left = `${i * zoomLevel}px`;
      hourLabels.appendChild(label);
    }
    hourLabels.style.width = `${1440 * zoomLevel}px`;
  }

  function renderTimeline() {
    timeSlider.innerHTML = "";
    previewGrid.innerHTML = "";
    buildTimeMarkers();
    timeSlider.style.width = `${1440 * zoomLevel}px`;

    timelineData.forEach(([timestamp, snapshots]) => {
      const [h, m] = timestamp.split(":");
      const minutes = parseInt(h) * 60 + parseInt(m);
      const left = minutes * zoomLevel;

      const matching = Object.entries(snapshots).filter(([cam]) =>
        selectedCameras.size === 0 || selectedCameras.has(cam)
      );
      if (matching.length === 0) return;

      const dot = document.createElement("div");
      dot.className = "dot";
      dot.style.left = `${left}px`;
      dot.title = timestamp;
      dot.setAttribute("tabindex", "-1");

      dot.addEventListener("mouseover", () => {
        showPreview(timestamp, matching);
      });

      timeSlider.appendChild(dot);
    });
  }

function showPreview(timestamp, matching) {
  lastTimestamp = timestamp;
  lastHoverTime = Date.now();

  // Create header once
  let header = previewGrid.querySelector(".previewHeader");
  if (!header) {
    header = document.createElement("h3");
    header.className = "previewHeader";
    previewGrid.appendChild(header);
  }
  header.textContent = timestamp;

  // Create snapshotRow once
  let row = previewGrid.querySelector(".snapshotRow");
  if (!row) {
    row = document.createElement("div");
    row.className = "snapshotRow";
    previewGrid.appendChild(row);
  }
  row.classList.toggle("centered", matching.length === 1);

  // Keep track of which cameras already have preview blocks
  const existing = {};
  row.querySelectorAll(".snapshotThumb").forEach((block) => {
    const camera = block.getAttribute("data-camera");
    existing[camera] = block;
  });

  matching.forEach(([camera, url]) => {
    let block = existing[camera];

    if (!block) {
      block = document.createElement("div");
      block.className = "snapshotThumb";
      block.setAttribute("data-camera", camera);

      const label = document.createElement("strong");
      label.textContent = camera;

      const crossfade = document.createElement("div");
      crossfade.className = "previewCrossfade";

      const currentImg = document.createElement("img");
      currentImg.className = "previewLayer";

      const incomingImg = document.createElement("img");
      incomingImg.className = "previewLayer top";

      crossfade.appendChild(currentImg);
      crossfade.appendChild(incomingImg);

      block.appendChild(label);
      block.appendChild(crossfade);
      row.appendChild(block);
    }

    const currentImg = block.querySelector(".previewLayer:not(.top)");
    const incomingImg = block.querySelector(".previewLayer.top");

    // Skip if it's already showing this image
    if (incomingImg.src === url || currentImg.src === url) return;

    const preload = new Image();
    preload.src = url;

    preload.onload = () => {
      incomingImg.src = url;
      incomingImg.style.opacity = "1";

      setTimeout(() => {
        currentImg.src = url;
        incomingImg.style.opacity = "0";
      }, 300);
    };
  });
}


  function fetchTimeline(date) {
    fetch(`/timeline/${date}`)
      .then((res) => res.json())
      .then((entries) => {
        timelineData = entries;
        imageCache.clear();
        lastTimestamp = null;

        const cameras = new Set();
        entries.forEach(([_, snapshots]) =>
          Object.keys(snapshots).forEach((cam) => cameras.add(cam))
        );

        cameraSelect.innerHTML = "";
        [...cameras].sort().forEach((camera) => {
          const opt = document.createElement("option");
          opt.value = camera;
          opt.textContent = camera;
          cameraSelect.appendChild(opt);
        });

        selectedCameras = new Set();
        renderTimeline();
      });
  }

  dateInput.addEventListener("change", () => {
    if (dateInput.value) fetchTimeline(dateInput.value);
  });

  cameraSelect.addEventListener("change", () => {
    selectedCameras = new Set(
      [...cameraSelect.selectedOptions].map((opt) => opt.value)
    );
    renderTimeline();
  });

  // 🖐 Drag-to-scroll
  let isDragging = false;
  let dragStartX = 0;
  let scrollStartX = 0;

  timelineWrapper.addEventListener("mousedown", (e) => {
    isDragging = true;
    dragStartX = e.clientX;
    scrollStartX = timelineWrapper.scrollLeft;
    timelineWrapper.classList.add("dragging");
  });

  document.addEventListener("mouseup", () => {
    isDragging = false;
    timelineWrapper.classList.remove("dragging");
  });

  document.addEventListener("mousemove", (e) => {
    if (isDragging) {
      const dx = dragStartX - e.clientX;
      timelineWrapper.scrollLeft = scrollStartX + dx;
      hourLabels.scrollLeft = timelineWrapper.scrollLeft;
    }
  });

let playbackTimer = null;
let isPlaying = false;
let currentPlaybackIndex = 0;

function togglePlayback() {
  const selectedCameras = new Set(
    [...cameraSelect.selectedOptions].map((opt) => opt.value)
  );
  const playButton = document.getElementById("playToggle");

  if (isPlaying) {
    clearInterval(playbackTimer);
    isPlaying = false;
    playButton.textContent = "▶️ Play";
    return;
  }

  if (timelineData.length === 0 || selectedCameras.size === 0) return;

  isPlaying = true;
  playButton.textContent = "⏸ Pause";
  currentPlaybackIndex = 0;

  playbackTimer = setInterval(() => {
    if (currentPlaybackIndex >= timelineData.length) {
      clearInterval(playbackTimer);
      isPlaying = false;
      playButton.textContent = "▶️ Play";
      return;
    }

    const [timestamp, snapshots] = timelineData[currentPlaybackIndex];

    const matching = Object.entries(snapshots).filter(([cam]) =>
      selectedCameras.has(cam)
    );

    if (matching.length > 0) {
      // Only update preview after image has fully loaded
const urlsToLoad = matching.map(([_, url]) => url);
const preload = new Image();
preload.src = urlsToLoad[0];

preload.onload = () => {
  showPreview(timestamp, matching);
  currentPlaybackIndex++;
};

preload.onerror = () => {
  currentPlaybackIndex++; // Skip failed frame
};

    }

  }, 500); // Playback speed in milliseconds
}
// Hook into DOM
document.getElementById("playToggle").addEventListener("click", togglePlayback);


  // 🎬 Scrubbing + floating timestamp
  timelineWrapper.addEventListener("mousemove", (e) => {
    const wrapperRect = timelineWrapper.getBoundingClientRect();
    const mouseX = e.clientX - wrapperRect.left;
    const minute = Math.round(mouseX / zoomLevel);
    const hh = String(Math.floor(minute / 60)).padStart(2, "0");
    const mm = String(minute % 60).padStart(2, "0");
    const timestamp = `${hh}:${mm}`;

    // Floating indicator
    timeIndicator.textContent = timestamp;
    timeIndicator.style.left = `${e.pageX + 10}px`;
    timeIndicator.style.top = `${wrapperRect.top + window.scrollY - 24}px`;
    timeIndicator.style.opacity = 1;

const entry = timelineData.find(([ts]) => ts === timestamp);

if (entry) {
  const matching = Object.entries(entry[1]).filter(([cam]) =>
    selectedCameras.size === 0 || selectedCameras.has(cam)
  );

  if (matching.length > 0) {
    showPreview(timestamp, matching);
  }
}

  });

  timelineWrapper.addEventListener("mouseleave", () => {
    timeIndicator.style.opacity = 0;
  });

  // 🔍 Zooming
  function zoomTimeline(byAmount, mouseX = null) {
    const rect = timelineWrapper.getBoundingClientRect();
    const centerX = mouseX !== null ? mouseX : rect.width / 2;
    const centerTime = (timelineWrapper.scrollLeft + centerX) / zoomLevel;

    zoomLevel = Math.min(Math.max(zoomLevel + byAmount, 1), 20);
    renderTimeline();

    requestAnimationFrame(() => {
      const newScrollLeft = centerTime * zoomLevel - centerX;
      timelineWrapper.scrollLeft = newScrollLeft;
      hourLabels.scrollLeft = newScrollLeft;
    });
  }

  zoomInBtn.addEventListener("click", () => zoomTimeline(0.5));
  zoomOutBtn.addEventListener("click", () => zoomTimeline(-0.5));

  timelineWrapper.addEventListener("wheel", (e) => {
    if (e.ctrlKey || e.shiftKey) {
      e.preventDefault();
      const mouseX = e.clientX - timelineWrapper.getBoundingClientRect().left;
      zoomTimeline(e.deltaY * -0.05, mouseX);
    }
  });

  if (dateInput.value) fetchTimeline(dateInput.value);
});