Skip to main content

ns-ui

Catenary Contact Stagger

A live connection/sync-quality indicator built from overhead contact wire stagger: a fixed pantograph marker tracks the wire's deliberate zigzag span by span, and only arcs, briefly, on the rare span where contact genuinely breaks.

Use when a live connection or sync-quality read where the ambient, continuous motion IS the signal and a rare fault should read as a genuine punctuation against a smooth, followable rhythm rather than a colour swap; pick status-glyph-cadence instead when the states are discrete task states (working/searching/blocked/done) that need five distinct at-a-glance cadences rather than one continuous sweep with an occasional rare event layered on top.

Install

npx shadcn add https://design.helpmarq.com/r/catenary-contact-stagger.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Source
registry/core/catenary-contact-stagger/component.tsx
"use client";

import { useEffect, useRef, useState } from "react";

// ---------------------------------------------------------------------------
// CatenaryContactStagger — a live connection/sync-quality indicator built
// from overhead contact wire stagger (OLE engineering). Contact wire is
// never laid dead straight above the track: it's deliberately zigzagged
// side to side, span by span, so a pantograph's carbon collector strip wears
// evenly across its width instead of grooving one spot. The strip
// continuously tracks that lateral zigzag as spans pass beneath it; on the
// rare span where contact genuinely breaks, a brief arc punctuates an
// otherwise smooth ride.
//
// Geometry is exact, not sampled: the stagger is a true engineering zigzag —
// dead straight spans between alternating extremes at every support — so the
// wire is a polyline with vertices only at span boundaries, where it flips
// from +stagger to -stagger. A single continuous phase u(t) = elapsed /
// SPAN_MS drives both the wire's horizontal scroll AND the fixed-position
// contact marker's lateral value, sampled from the exact same triangle
// function, so the marker never drifts out of sync with the wire it's
// supposedly riding. One sweep leg (extreme to extreme) takes SPAN_MS; the
// marker's lateral position is therefore always continuous and always
// legible at a glance, at a speed (tens of px/s) nowhere near 60Hz paint.
//
// The dewirement arc is a separate, decoupled Poisson-ish process (mean
// interval independent of the sweep) so it reads as a genuine rare event,
// not a beat in the main rhythm: a brief luminance flash plus a small
// vertical wire "kick" that decays back to the smooth zigzag it interrupted.
// A `connectionQuality` prop (0-1, default 1) shortens the arc's mean
// interval as quality degrades — severity reads via event rate, never hue.
//
// Pure DOM/SVG, no canvas. Wire, strip band and marker are all
// var(--foreground) strokes/fills at full weight in both themes (the CSS var
// itself repaints on a theme swap, no JS token re-read needed since nothing
// here touches a canvas context). The arc flash uses an SVG drop-shadow
// filter seeded from var(--foreground) — glows light in dark mode, glows
// dark in light mode automatically, never introduces a colour of its own.
// ---------------------------------------------------------------------------

const SPAN_MS = 2000; // one sweep leg (extreme -> extreme) per REAL NUMBERS
const ARC_MS = 180; // dewirement arc duration
const ARC_KICK_PX = 3; // vertical wire "kick" during an arc
const DEFAULT_MIN_ARC_MS = 18000;
const DEFAULT_MAX_ARC_MS = 28000;
// degraded connections never arc faster than 4 sweep legs apart — any
// tighter and the arc joins the sweep's own rhythm instead of punctuating it.
const MIN_ARC_FLOOR_MS = SPAN_MS * 4;
const VISIBLE_SPANS = 4; // spans visible across the container at rest
const BAND_FRACTION = 0.68; // strip band height, as a fraction of min(w,h)
const AMPLITUDE_FRACTION = 0.2; // stagger half-throw, as a fraction of min(w,h) — stays inside the band with margin even mid-arc
const MARKER_R = 3.5; // px, contact marker radius at rest
const MARKER_R_ARC = 5.5; // px, contact marker radius during an arc
const WIRE_WIDTH = 1.4; // px, wire stroke at rest
const WIRE_WIDTH_ARC = 3; // px, wire stroke during an arc — weight carries the flash in both themes

/** Exact engineering zigzag: straight spans between alternating extremes.
 * Period 2 (one full extreme -> extreme -> extreme cycle), range -1..1,
 * piecewise-linear so vertices at every integer u ARE the true shape —
 * no sampling, no aliasing. */
function triWave(u: number): number {
  const m = ((u % 2) + 2) % 2; // 0..2
  return m < 1 ? m * 2 - 1 : 3 - m * 2;
}

function useReducedMotion() {
  const [reduced, setReduced] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    setReduced(mq.matches);
    const onChange = () => setReduced(mq.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, []);
  return reduced;
}

export interface CatenaryContactStaggerProps {
  /** 0-1, default 1 (perfect). Degraded connectivity shortens the arc's mean
   * interval — severity reads via event rate, never colour. */
  connectionQuality?: number;
  /** floor of the randomised arc interval range at connectionQuality = 1 */
  minArcIntervalMs?: number;
  /** ceiling of the randomised arc interval range at connectionQuality = 1 */
  maxArcIntervalMs?: number;
  /** accessible label for the root status region */
  label?: string;
  /** extra classes merged onto the root element */
  className?: string;
}

export function CatenaryContactStagger({
  connectionQuality = 1,
  minArcIntervalMs = DEFAULT_MIN_ARC_MS,
  maxArcIntervalMs = DEFAULT_MAX_ARC_MS,
  label = "Connection sync status",
  className = "",
}: CatenaryContactStaggerProps) {
  const reduced = useReducedMotion();
  const rootRef = useRef<HTMLDivElement>(null);
  const svgRef = useRef<SVGSVGElement>(null);
  const wireRef = useRef<SVGPolylineElement>(null);
  const bandRef = useRef<SVGRectElement>(null);
  const markerRef = useRef<SVGCircleElement>(null);
  const [arcing, setArcing] = useState(false);
  const [announce, setAnnounce] = useState("Contact wire tracking nominal.");
  const quality = Math.max(0, Math.min(1, connectionQuality));

  useEffect(() => {
    const root = rootRef.current;
    const svg = svgRef.current;
    const wire = wireRef.current;
    const band = bandRef.current;
    const marker = markerRef.current;
    if (!root || !svg || !wire || !band || !marker) return;

    let disposed = false;
    let visible = true;
    let w = 0;
    let h = 0;
    let sized = false;
    let spanWidth = 1;
    let amplitude = 1;
    let bandCenterY = 0;
    let bandHeight = 0;
    // origin of the phase clock — set once, on first successful measure, and
    // never reset afterward. A visibility pause/resume (tab hidden, card
    // scrolled off) must NOT jump the phase back to a t0 extreme: the wire
    // is a continuous physical process, so elapsed time keeps accumulating
    // against wall-clock time whether or not a frame was painted for it.
    let originTime = 0;
    let raf = 0;
    let arcStart: number | null = null;
    let arcTimer = 0;
    let arcEndTimer = 0;
    let arcPending = false;

    const arcInterval = () => {
      const min = Math.max(MIN_ARC_FLOOR_MS, minArcIntervalMs * quality || MIN_ARC_FLOOR_MS);
      const max = Math.max(min + 500, maxArcIntervalMs * quality || min + 500);
      return min + Math.random() * (max - min);
    };

    const cancelArcSchedule = () => {
      window.clearTimeout(arcTimer);
      arcTimer = 0;
      arcPending = false;
    };

    const scheduleArc = () => {
      if (disposed || reduced || arcPending) return;
      arcPending = true;
      arcTimer = window.setTimeout(() => {
        if (disposed) return;
        arcPending = false;
        arcStart = performance.now();
        setArcing(true);
        setAnnounce("Momentary contact loss.");
        arcEndTimer = window.setTimeout(() => {
          if (disposed) return;
          arcStart = null;
          setArcing(false);
          setAnnounce("Contact wire tracking nominal.");
          if (visible) scheduleArc();
        }, ARC_MS);
      }, arcInterval());
    };

    const measure = () => {
      const rect = root.getBoundingClientRect();
      w = rect.width;
      h = rect.height;
      if (w < 2 || h < 2) {
        sized = false;
        return;
      }
      sized = true;
      const minDim = Math.min(w, h);
      spanWidth = w / VISIBLE_SPANS;
      amplitude = minDim * AMPLITUDE_FRACTION;
      bandHeight = minDim * BAND_FRACTION;
      bandCenterY = h / 2;
      svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
      band.setAttribute("x", "0");
      band.setAttribute("y", `${bandCenterY - bandHeight / 2}`);
      band.setAttribute("width", `${w}`);
      band.setAttribute("height", `${bandHeight}`);
    };

    // exact zigzag vertices: straight spans between alternating extremes,
    // vertex n sits at screenX = centerX + spanWidth * (n - u), y alternates
    // -amplitude / +amplitude by n's parity — the true engineering shape,
    // not a sampled approximation.
    const render = (u: number, kick: number) => {
      const centerX = w / 2;
      const half = VISIBLE_SPANS / 2;
      const nMin = Math.floor(u - half) - 2;
      const nMax = Math.ceil(u + half) + 2;
      const pts: string[] = [];
      for (let n = nMin; n <= nMax; n++) {
        const x = centerX + spanWidth * (n - u);
        const y = bandCenterY + amplitude * (n % 2 === 0 ? -1 : 1) + kick;
        pts.push(`${x.toFixed(1)},${y.toFixed(1)}`);
      }
      wire.setAttribute("points", pts.join(" "));
      const contactY = bandCenterY + amplitude * triWave(u) + kick;
      marker.setAttribute("cy", `${contactY.toFixed(2)}`);
      marker.setAttribute("cx", `${centerX.toFixed(2)}`);
    };

    const loop = (now: number) => {
      raf = 0;
      if (!visible || !sized) return;
      const elapsed = now - originTime;
      const u = elapsed / SPAN_MS;
      let kick = 0;
      if (arcStart != null) {
        const p = Math.min(1, (now - arcStart) / ARC_MS);
        kick = ARC_KICK_PX * Math.sin(Math.PI * p);
      }
      render(u, kick);
      raf = requestAnimationFrame(loop);
    };

    // measures the container and, once sized, (re)activates whatever isn't
    // already running — the rAF loop and the arc scheduler. Safe to call
    // repeatedly (from resize, from the font-ready settle, from IO resume):
    // it's idempotent whenever the loop/scheduler are already live.
    const activate = () => {
      measure();
      if (!sized) return; // will retry on the next resize/IO callback
      if (originTime === 0) originTime = performance.now();
      if (reduced) {
        // dead-centre, maximum contact margin — the "everything is fine"
        // frame, never a stagger extreme and never mid-arc.
        render(0.5, 0);
        return;
      }
      if (visible && !raf) raf = requestAnimationFrame(loop);
      if (visible) scheduleArc();
    };

    activate();

    let resizeTimer = 0;
    const onResize = () => {
      window.clearTimeout(resizeTimer);
      resizeTimer = window.setTimeout(() => {
        if (disposed) return;
        activate();
      }, 80);
    };
    const ro = new ResizeObserver(onResize);
    ro.observe(root);

    const io = new IntersectionObserver((entries) => {
      visible = entries[0]?.isIntersecting ?? true;
      if (visible) {
        activate();
      } else {
        cancelArcSchedule(); // don't keep announcing arcs nobody can see
      }
    });
    io.observe(root);

    document.fonts.ready.then(() => {
      if (!disposed) onResize();
    });

    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      raf = 0;
      cancelArcSchedule();
      window.clearTimeout(arcEndTimer);
      window.clearTimeout(resizeTimer);
      ro.disconnect();
      io.disconnect();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [reduced, quality, minArcIntervalMs, maxArcIntervalMs]);

  return (
    <div
      ref={rootRef}
      role="group"
      aria-label={label}
      className={`relative h-full w-full ${className}`}
    >
      <svg
        ref={svgRef}
        aria-hidden="true"
        focusable="false"
        className="pointer-events-none block h-full w-full"
      >
        <defs>
          <filter id="ccs-arc-glow" x="-200%" y="-200%" width="500%" height="500%">
            <feDropShadow dx="0" dy="0" stdDeviation="2.4" floodColor="var(--foreground)" floodOpacity="0.9" />
          </filter>
        </defs>

        {/* strip band — fixed reference, the zigzag must stay inside it */}
        <rect
          ref={bandRef}
          fill="none"
          stroke="var(--foreground)"
          strokeOpacity={0.28}
          strokeWidth={1}
        />

        {/* contact wire — exact zigzag polyline, span-by-span alternation.
            An arc flash reads through WEIGHT (stroke bumps up), not colour —
            the glow filter is secondary polish, never the sole signal, so
            it still reads against a light card. */}
        <polyline
          ref={wireRef}
          fill="none"
          stroke="var(--foreground)"
          strokeWidth={arcing ? WIRE_WIDTH_ARC : WIRE_WIDTH}
          strokeLinejoin="round"
          strokeLinecap="round"
          style={{
            filter: arcing ? "url(#ccs-arc-glow)" : "none",
            opacity: arcing ? 1 : 0.85,
            transition: "opacity 60ms linear, stroke-width 40ms ease-out",
          }}
        />

        {/* contact marker — the one followable thing, riding the wire's
            lateral value at the pantograph's fixed position. Starts off the
            visible canvas until the first measured frame writes real
            coordinates, so there's never a stray dot at the SVG origin. */}
        <circle
          ref={markerRef}
          cx={-999}
          cy={-999}
          r={arcing ? MARKER_R_ARC : MARKER_R}
          fill="var(--foreground)"
          style={{
            filter: arcing ? "url(#ccs-arc-glow)" : "none",
            transition: "r 80ms ease-out",
          }}
        />
      </svg>

      <p role="status" aria-live="polite" className="sr-only">
        {announce}
      </p>
    </div>
  );
}
Build spec

Renders a live connection/sync-quality indicator as overhead contact wire stagger (OLE engineering): contact wire is deliberately zigzagged side to side, span by span, so a pantograph's carbon collector strip wears evenly across its width rather than grooving one spot. A fixed-width strip band (a thin var(--foreground) stroked rectangle at 0.28 opacity, sized to the container's smaller dimension x 0.68, centred vertically) is the static reference the zigzag must stay inside. The wire itself is an exact engineering zigzag, not a sampled curve: straight spans between alternating +/- extremes at every support, rendered as a <polyline> whose vertices are computed directly from a single continuous phase u(t) = elapsedMs / 2000 (one sweep leg, extreme to extreme, takes 2.0s) via triWave(n) = alternating -1/+1 at every integer n, vertex screenX = centerX + spanWidth * (n - u(t)) where spanWidth = containerWidth / 4 (four visible spans at rest). A small filled circle at the container's fixed horizontal centre — the pantograph's contact point — samples the exact same triWave(u(t)) function (not a re-derivation), so the marker's lateral position is always perfectly consistent with the wire riding under it; this marker is the one followable thing, per the legibility line, and its cadence is exactly the wire's: one full sweep every 2.0s. On the rare span where contact genuinely breaks, a dewirement arc fires: a self-rescheduling setTimeout loop (never a fixed interval, and never re-armed while it's already pending) draws a mean interval from a uniform 18-28s range at connectionQuality=1, scaled down as the optional connectionQuality prop (0-1, default 1) drops toward 0 — floored at 4 sweep legs (8s) so a degraded connection still reads as a distinct rare punctuation rather than joining the sweep's own 2s rhythm — severity reads via event rate, never colour or hue. Each arc lasts ~180ms and reads primarily through WEIGHT, not colour, so it survives a light card: the wire's stroke and the marker's radius both step up for the duration (1.4px to 3px, 3.5px to 5.5px), a Math.sin(pi * progress) vertical kick of up to 3px is added to both the wire polyline and the marker's y (decaying back to zero at both ends, so it reads as a bump, not a snap), and an SVG feDropShadow filter seeded from var(--foreground) rides along as secondary polish — a pure luminance glow, never accent, that renders as a light halo in dark theme and a dark halo in light theme automatically since it's built from the same token rather than a literal. The whole apparatus is direct-DOM: geometry is computed inside a single rAF loop that writes the polyline's `points` and the circle's cx/cy attributes on every frame via refs, no React state on the animation hot path (arcing is a low-frequency boolean flip, twice per arc, used only to toggle stroke weight/radius/filter). A ResizeObserver re-measures the container and recomputes spanWidth/amplitude/band geometry (both derived from the container's SMALLER dimension so it stays legible at card scale) and, if the container wasn't yet laid out at mount (0px on first measure — a hidden tab, an unlaid-out catalog card), retries activation on every subsequent resize until it succeeds. The phase clock's origin is set exactly once, on the first successful measure, and never reset: an IntersectionObserver pauses the rAF loop off-screen and cancels only the pending next-arc timer (an arc already 180ms into flight finishes naturally), but resuming on re-entry continues the same wall-clock phase rather than snapping back to a t0 extreme, since the wire is a continuous process whether or not a frame was painted for it. prefers-reduced-motion freezes on triWave(0.5) — dead centre over the strip, maximum contact margin, the 'everything is fine' frame — never at a stagger extreme and never mid-arc; the arc scheduler simply never starts in this mode. Accessibility: the root is role=group with an accessible label describing the connection this indicator reflects; a separate sr-only role=status aria-live=polite paragraph holds only 'Contact wire tracking nominal.' or, for the ~180ms of an arc, 'Momentary contact loss.', reverting immediately after — kept out of the labelled group so aria-atomic re-reads never duplicate the group's own label. No interaction: this is a pure ambient status read, nothing latches, nothing can be pressed. Props: connectionQuality (0-1, optional), minArcIntervalMs/maxArcIntervalMs (override the 18-28s range at quality=1), label, className. Zero dependencies, no canvas, pure SVG/DOM.

Props

PropTypeDefaultDescription
connectionQuality?number10-1, default 1 (perfect). Degraded connectivity shortens the arc's mean interval — severity reads via event rate, never colour.
minArcIntervalMs?numberDEFAULT_MIN_ARC_MSfloor of the randomised arc interval range at connectionQuality = 1
maxArcIntervalMs?numberDEFAULT_MAX_ARC_MSceiling of the randomised arc interval range at connectionQuality = 1
label?string"Connection sync status"accessible label for the root status region
className?stringextra classes merged onto the root element