<!-- TRACES: UR-007 | DR-007 -->
<script lang="ts">
  /**
   * A vertical A-Z index strip for long, alphabetically-sorted lists.
   * Tapping or dragging a letter jumps to the first item that starts with it.
   *
   * The parent owns the actual scrolling: it passes `availableLetters`
   * (which letters have items) and an `onJump(letter)` callback.
   */

  const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
  const HASH = "#"; // bucket for names starting with a digit/symbol

  interface Props {
    /** Uppercase letters (or "#") that currently have at least one item. */
    availableLetters: Set<string>;
    /** Called with the chosen letter when the user picks one. */
    onJump: (letter: string) => void;
    /**
     * CSS length reserved at the bottom of the viewport for the bottom nav /
     * mini-player bars. The strip stretches to fill the space between the top
     * sticky offset and this gap, so it ends just above those bars.
     */
    bottomGap?: string;
  }

  let { availableLetters, onJump, bottomGap = "5rem" }: Props = $props();

  // The strip stretches from where it sits down to just above the bottom nav /
  // mini-player bars. Those bars are pinned to the bottom of the screen, so the
  // hard floor is `window.innerHeight - bottomGap`. We measure the strip's own
  // top against that floor (clamped to non-negative) and update on scroll/resize
  // so it never slides under the bars regardless of header or platform.
  let container = $state<HTMLDivElement | null>(null);
  let stripHeight = $state(0);

  function measure() {
    if (!container) return;
    const top = container.getBoundingClientRect().top;
    const floor = window.innerHeight - remToPx(bottomGap);
    stripHeight = Math.max(0, floor - top);
  }

  function remToPx(len: string): number {
    const n = parseFloat(len);
    if (len.trim().endsWith("rem")) {
      const root = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
      return n * root;
    }
    return n; // assume px otherwise
  }

  $effect(() => {
    measure();
    const scroller = container?.closest("main");
    const ro = new ResizeObserver(measure);
    if (scroller) ro.observe(scroller);
    scroller?.addEventListener("scroll", measure, { passive: true });
    window.addEventListener("resize", measure);
    return () => {
      ro.disconnect();
      scroller?.removeEventListener("scroll", measure);
      window.removeEventListener("resize", measure);
    };
  });

  // Recompute when the reserved bottom gap changes (mini-player shows/hides).
  $effect(() => {
    // Bare read: registers `bottomGap` as a dependency of this effect. Svelte 5
    // idiom, not a stray expression.
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    bottomGap;
    measure();
  });

  const letters = $derived([HASH, ...ALPHABET]);
  let activeLetter = $state<string | null>(null);

  function jumpTo(letter: string) {
    if (!availableLetters.has(letter)) return;
    activeLetter = letter;
    onJump(letter);
  }

  // Allow dragging a finger/mouse down the strip to scrub through letters.
  function letterAt(clientY: number): string | null {
    const el = document.elementFromPoint(
      // x is supplied by the caller via the bound container; we read from the
      // element under the pointer instead to stay layout-agnostic.
      lastClientX,
      clientY,
    );
    const letter = el?.getAttribute?.("data-letter");
    return letter ?? null;
  }

  let lastClientX = $state(0);
  let isScrubbing = $state(false);

  function handlePointerDown(e: PointerEvent) {
    isScrubbing = true;
    lastClientX = e.clientX;
    const letter = letterAt(e.clientY);
    if (letter) jumpTo(letter);
  }

  function handlePointerMove(e: PointerEvent) {
    if (!isScrubbing) return;
    lastClientX = e.clientX;
    const letter = letterAt(e.clientY);
    if (letter && letter !== activeLetter) jumpTo(letter);
  }

  function handlePointerUp() {
    isScrubbing = false;
  }
</script>

<svelte:window onpointerup={handlePointerUp} onpointercancel={handlePointerUp} />

<div
  bind:this={container}
  class="flex flex-col items-center justify-between select-none touch-none py-1"
  style="height: {stripHeight}px"
  onpointerdown={handlePointerDown}
  onpointermove={handlePointerMove}
  role="navigation"
  aria-label="Jump to letter"
>
  {#each letters as letter (letter)}
    {@const enabled = availableLetters.has(letter)}
    <button
      type="button"
      data-letter={letter}
      disabled={!enabled}
      onclick={() => jumpTo(letter)}
      class="w-5 leading-tight text-[10px] sm:text-xs font-semibold transition-colors
        {enabled
        ? 'text-gray-400 hover:text-[var(--color-jellyfin)]'
        : 'text-gray-700 cursor-default'}
        {activeLetter === letter && enabled ? 'text-[var(--color-jellyfin)] scale-125' : ''}"
      aria-label={`Jump to ${letter}`}
    >
      {letter}
    </button>
  {/each}
</div>
