# Signal Beam

> A neon tube drawn between two elements, with a pulse of light running along it. Curves, reverses, runs both ways.

Category: Effects. Docs: https://neonkit.heyaadi.com/components/signal-beam

## Installation

```bash
npx shadcn@latest add https://neonkit.heyaadi.com/r/signal-beam.json
```

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/signal-beam`.

npm dependencies: none. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { SignalBeam } from "@/components/ui/signal-beam"

const container = useRef(null)
const from = useRef(null)
const to = useRef(null)

<div ref={container} className="relative flex justify-between">
  <span ref={from}>…</span>
  <span ref={to}>…</span>
  <SignalBeam containerRef={container} fromRef={from} toRef={to} curvature={30} />
</div>
```

## Examples

### Integrations hub

Five sources feed one hub. The paused one keeps its tube but carries nothing.

```tsx
"use client";

import * as React from "react";
import { Bug, CreditCard, Database, Github, Snowflake, SquareKanban, Webhook } from "@/components/ui/icons";
import { SignalBeam } from "@/components/ui/signal-beam";

const sources = [
  { name: "GitHub", detail: "push, pull_request", icon: Github, curvature: 46 },
  { name: "Stripe", detail: "invoice.paid", icon: CreditCard, curvature: 22 },
  { name: "Postgres", detail: "orders, CDC", icon: Database, curvature: 0 },
  { name: "Linear", detail: "issue.updated", icon: SquareKanban, curvature: -22 },
  { name: "Sentry", detail: "paused", icon: Bug, curvature: -46, paused: true },
];

function Tile({ children, className = "" }: { children: React.ReactNode; className?: string }) {
  return (
    <span
      className={`relative z-10 grid size-10 shrink-0 place-items-center rounded-xl border border-white/10 bg-[#0b0d14] text-white/80 sm:size-11 [&_svg]:size-[18px] ${className}`}
    >
      {children}
    </span>
  );
}

export default function SignalBeamDemo() {
  const containerRef = React.useRef<HTMLDivElement>(null);
  const hubRef = React.useRef<HTMLSpanElement>(null);
  const sinkRef = React.useRef<HTMLSpanElement>(null);
  const r0 = React.useRef<HTMLSpanElement>(null);
  const r1 = React.useRef<HTMLSpanElement>(null);
  const r2 = React.useRef<HTMLSpanElement>(null);
  const r3 = React.useRef<HTMLSpanElement>(null);
  const r4 = React.useRef<HTMLSpanElement>(null);
  const refs = [r0, r1, r2, r3, r4];

  return (
    <div
      ref={containerRef}
      className="relative flex w-full max-w-2xl items-center justify-between gap-4 py-2"
    >
      <ul className="flex flex-col gap-4 sm:min-w-[132px] sm:gap-5">
        {sources.map((source, i) => {
          const Icon = source.icon;
          return (
            <li key={source.name} className="flex items-center justify-end gap-3">
              <span className="hidden min-w-0 text-right sm:block">
                <span className={`block text-sm ${source.paused ? "text-white/45" : "text-white/85"}`}>{source.name}</span>
                <span className="block font-mono text-[11px] text-white/45">{source.detail}</span>
              </span>
              <Tile className={source.paused ? "text-white/35" : ""}>
                <span ref={refs[i]} className="absolute inset-0 rounded-xl" />
                <Icon />
              </Tile>
            </li>
          );
        })}
      </ul>

      <div className="relative">
        <span
          ref={hubRef}
          className="relative z-10 grid size-16 place-items-center rounded-full border border-[rgb(var(--neon)/0.55)] bg-[#0b0d14] text-[rgb(var(--neon))] shadow-[0_0_0_4px_rgb(var(--neon)/0.06),0_0_28px_rgb(var(--neon)/CALC(0.35*var(--neon-glow,1)))] sm:size-20 [&_svg]:size-7"
        >
          <Webhook />
        </span>
        <span className="absolute left-1/2 top-full mt-3 -translate-x-1/2 whitespace-nowrap text-center">
          <span className="block text-sm text-white/85">Relay</span>
          <span className="block font-mono text-[11px] tabular-nums text-white/55">1,284 events/min</span>
        </span>
      </div>

      <div className="flex items-center gap-3 sm:min-w-[132px]">
        <Tile>
          <span ref={sinkRef} className="absolute inset-0 rounded-xl" />
          <Snowflake />
        </Tile>
        <span className="hidden sm:block">
          <span className="block text-sm text-white/85">Snowflake</span>
          <span className="block font-mono text-[11px] text-white/45">RAW.EVENTS</span>
        </span>
      </div>

      {sources.map((source, i) => (
        <SignalBeam
          key={source.name}
          containerRef={containerRef}
          fromRef={refs[i]}
          toRef={hubRef}
          curvature={source.curvature}
          delay={[0, 1.3, 0.5, 1.9, 0][i]}
          duration={1.8}
          repeatDelay={1.4}
          active={!source.paused}
        />
      ))}
      <SignalBeam containerRef={containerRef} fromRef={hubRef} toRef={sinkRef} duration={1.1} repeatDelay={0.5} delay={0.9} pulseLength={0.45} />
    </div>
  );
}
```

### Reverse and both ways

A request goes out and a response comes back on the same tube, half a cycle apart.

```tsx
"use client";

import * as React from "react";
import { Laptop, Server } from "@/components/ui/icons";
import { SignalBeam } from "@/components/ui/signal-beam";

const rows = [
  { label: "POST /v1/events", note: "default", props: {} },
  { label: "Server-sent events", note: "reverse", props: { reverse: true } },
  { label: "WebSocket", note: "bidirectional", props: { bidirectional: true, duration: 1.6, repeatDelay: 0.6 } },
];

function Node({ children, nodeRef }: { children: React.ReactNode; nodeRef: React.RefObject<HTMLSpanElement | null> }) {
  return (
    <span
      ref={nodeRef}
      className="relative z-10 grid size-10 shrink-0 place-items-center rounded-xl border border-white/10 bg-[#0b0d14] text-white/75 [&_svg]:size-[18px]"
    >
      {children}
    </span>
  );
}

function Row({ label, note, props }: (typeof rows)[number]) {
  const container = React.useRef<HTMLDivElement>(null);
  const client = React.useRef<HTMLSpanElement>(null);
  const server = React.useRef<HTMLSpanElement>(null);
  return (
    <div ref={container} className="relative flex items-center gap-4">
      <Node nodeRef={client}>
        <Laptop />
      </Node>
      <div className="flex-1 pb-9 text-center">
        <span className="text-[13px] text-white/80">{label}</span>
        <span className="ml-2 font-mono text-[11px] text-white/40">{note}</span>
      </div>
      <Node nodeRef={server}>
        <Server />
      </Node>
      <SignalBeam containerRef={container} fromRef={client} toRef={server} {...props} />
    </div>
  );
}

export default function SignalBeamDirections() {
  return (
    <div className="grid w-full max-w-md gap-7">
      {rows.map((row) => (
        <Row key={row.label} {...row} />
      ))}
    </div>
  );
}
```

### Live, slow and dark

Glow marks a link that's carrying traffic; a dark tube is dashed and unlit.

```tsx
"use client";

import * as React from "react";
import { GitFork, Server } from "@/components/ui/icons";
import { SignalBeam } from "@/components/ui/signal-beam";

const regions = [
  { name: "eu-west-1", detail: "1,920 req/s", curvature: 34, beam: { duration: 1.1, repeatDelay: 0.2 } },
  { name: "ap-south-1", detail: "40 req/s", curvature: 0, beam: { duration: 2.6, repeatDelay: 2.4 } },
  { name: "us-east-2", detail: "failover, idle", curvature: -34, beam: { active: false } },
];

export default function SignalBeamStates() {
  const container = React.useRef<HTMLDivElement>(null);
  const balancer = React.useRef<HTMLSpanElement>(null);
  const r0 = React.useRef<HTMLSpanElement>(null);
  const r1 = React.useRef<HTMLSpanElement>(null);
  const r2 = React.useRef<HTMLSpanElement>(null);
  const refs = [r0, r1, r2];

  return (
    <div ref={container} className="relative flex w-full max-w-md items-center justify-between gap-10">
      <div className="grid justify-items-center gap-2">
        <span
          ref={balancer}
          className="relative z-10 grid size-12 place-items-center rounded-full border border-[rgb(var(--neon)/0.5)] bg-[#0b0d14] text-[rgb(var(--neon))] [&_svg]:size-5"
        >
          <GitFork />
        </span>
        <span className="text-[13px] text-white/60">lb-prod</span>
      </div>
      <ul className="grid gap-5">
        {regions.map((region, i) => (
          <li key={region.name} className="flex items-center gap-3">
            <span
              ref={refs[i]}
              className={`relative z-10 grid size-9 shrink-0 place-items-center rounded-lg border border-white/10 bg-[#0b0d14] [&_svg]:size-4 ${
                region.beam.active === false ? "text-white/30" : "text-white/75"
              }`}
            >
              <Server />
            </span>
            <span>
              <span className="block font-mono text-[12px] text-white/85">{region.name}</span>
              <span className="block text-[12px] text-white/45">{region.detail}</span>
            </span>
          </li>
        ))}
      </ul>
      {regions.map((region, i) => (
        <SignalBeam
          key={region.name}
          containerRef={container}
          fromRef={balancer}
          toRef={refs[i]}
          curvature={region.curvature}
          {...region.beam}
        />
      ))}
    </div>
  );
}
```

## API reference

### SignalBeam

An SVG that fills its positioned parent. Render it inside the container, after or before the nodes. Also accepts native SVG attributes.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `fromRef` | `RefObject<HTMLElement>` | — | Where the tube starts. |
| `toRef` | `RefObject<HTMLElement>` | — | Where the tube ends. |
| `containerRef` | `RefObject<HTMLElement>` | `parent` | Watched for resizes along with both nodes. |
| `curvature` | `number` | `0` | Bow at the middle in pixels. Positive bends left of the direction of travel. |
| `duration` | `number` | `2.4` | Seconds for a pulse to cross. |
| `delay` | `number` | `0` | Seconds before the first pulse. Stagger beams with it. |
| `repeatDelay` | `number` | `0.8` | Seconds of dark between pulses. |
| `reverse` | `boolean` | `false` | Run from `to` back to `from`. |
| `bidirectional` | `boolean` | `false` | Pulses both ways, half a cycle apart. |
| `pulseLength` | `number` | `0.3` | Pulse length as a share of the tube, 0.05 to 1. |
| `width` | `number` | `2` | Tube thickness in pixels. |
| `active` | `boolean` | `true` | False leaves a dashed, unlit tube with no pulse. |
| `gap` | `number` | `4` | Pixels between a node's edge and the tube's end. |
| `fromOffset` | `[number, number]` | — | Nudges the start point, in pixels. |
| `toOffset` | `[number, number]` | — | Nudges the end point, in pixels. |
| `color` | `NeonColor` | `inherited` | Color of the tube and its light. |

## Accessibility

- Decorative: the SVG is aria-hidden. Describe the connection in text if it carries meaning.
- Ends leave each node along the tube's tangent, from the edge of a box or the rim of a round node, so the tube never starts under a card.
- The pulse is CSS animating a dash offset on a path with pathLength 1. No script runs per frame, and a resize only rewrites the path.
- Pulses pause off screen and in background tabs. With reduced motion each pulse holds still, partway along its tube.
- Inside a scaled parent (a zoomed preview) the geometry is corrected for the scale.

## Source

`components/ui/signal-beam.tsx`

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

// The pulse is a stack of dashes on one path, each shorter and hotter than the one under it, all ending
// at the same head: a dim tail, a lit body, a white-hot tip, and two wide faint strokes for the halo. The
// path has pathLength 1, so the dash maths is in shares of the path and survives any resize; only the
// dash offset animates, from CSS, with no script per frame.
const css = `
.nk-signal-beam-pulse { stroke-dashoffset: var(--nk-sb-a); animation: var(--nk-sb-name) var(--nk-sb-cycle) linear var(--nk-sb-delay) infinite both; }
.nk-signal-beam-flash { opacity: 0; animation: var(--nk-sb-flash) var(--nk-sb-cycle) linear var(--nk-sb-delay) infinite both; }
.nk-signal-beam[data-paused] .nk-signal-beam-pulse,
.nk-signal-beam[data-paused] .nk-signal-beam-flash { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .nk-signal-beam-pulse { animation: none; stroke-dashoffset: var(--nk-sb-still); }
  .nk-signal-beam-flash { animation: none; opacity: 0; }
}
`;

// Share of the path left dark either side of the pulse, so no round cap shows before or after a run.
const EDGE = 0.002;

type Layer = { length: number; width: number; stroke: string };

// The halo strokes get shorter as they get wider, so the light tapers off the head instead of ending in a capsule.
const layers = (w: number): Layer[] => [
  { length: 0.5, width: w * 6.5, stroke: "rgb(var(--neon) / calc(0.05 * var(--neon-glow, 1)))" },
  { length: 0.7, width: w * 4, stroke: "rgb(var(--neon) / calc(0.09 * var(--neon-glow, 1)))" },
  { length: 0.9, width: w * 2.2, stroke: "rgb(var(--neon) / calc(0.2 * var(--neon-glow, 1)))" },
  { length: 1, width: w, stroke: "rgb(var(--neon) / 0.3)" },
  { length: 0.62, width: w, stroke: "rgb(var(--neon) / 0.7)" },
  { length: 0.28, width: w, stroke: "color-mix(in srgb, rgb(var(--neon)) 45%, white)" },
  { length: 0.08, width: w * 0.9, stroke: "#fff" },
];

type Geometry = { d: string; ax: number; ay: number; bx: number; by: number };

function radiusOf(node: Element, w: number, h: number) {
  const style = getComputedStyle(node);
  const raw = style.borderTopLeftRadius || style.borderRadius || "0";
  const value = parseFloat(raw) || 0;
  return raw.trim().endsWith("%") ? (value / 100) * Math.min(w, h) : value;
}

/** Where a ray from the centre of a box leaves it: an ellipse for round nodes, the box for the rest. */
function exit(hw: number, hh: number, round: boolean, dx: number, dy: number) {
  const len = Math.hypot(dx, dy) || 1;
  const ux = dx / len;
  const uy = dy / len;
  if (hw <= 0 || hh <= 0) return 0;
  if (round) return 1 / Math.sqrt((ux / hw) ** 2 + (uy / hh) ** 2);
  return Math.min(ux ? hw / Math.abs(ux) : Infinity, uy ? hh / Math.abs(uy) : Infinity);
}


export interface SignalBeamProps
  extends Omit<React.SVGAttributes<SVGSVGElement>, "color" | "children"> {
  /** The positioned element both nodes sit in. The beam fills it; place `<SignalBeam>` as its child. */
  containerRef?: React.RefObject<HTMLElement | null>;
  /** Where the tube starts. */
  fromRef: React.RefObject<HTMLElement | null>;
  /** Where the tube ends. */
  toRef: React.RefObject<HTMLElement | null>;
  /** Palette name or any hex color. Inherits `--neon-accent` when omitted. */
  color?: NeonColor;
  /** Bow of the tube in pixels, at its middle. Positive bends to the left of the direction of travel (up, going right). */
  curvature?: number;
  /** Seconds for a pulse to cross. */
  duration?: number;
  /** Seconds before the first pulse. Stagger beams with it. */
  delay?: number;
  /** Seconds of dark between pulses. */
  repeatDelay?: number;
  /** Run the pulse from `to` back to `from`. */
  reverse?: boolean;
  /** Pulses run both ways, half a cycle apart. */
  bidirectional?: boolean;
  /** Length of the pulse as a share of the tube, 0.05 to 1. */
  pulseLength?: number;
  /** Tube thickness in pixels. */
  width?: number;
  /** A dark tube carries no signal: no pulse, no glow. */
  active?: boolean;
  /** Pixels between a node's edge and the end of the tube. */
  gap?: number;
  /** Nudges the start point, in pixels. */
  fromOffset?: [number, number];
  /** Nudges the end point, in pixels. */
  toOffset?: [number, number];
}

const SignalBeam = React.forwardRef<SVGSVGElement, SignalBeamProps>(
  (
    {
      className,
      style,
      color,
      containerRef,
      fromRef,
      toRef,
      curvature = 0,
      duration = 2.4,
      delay = 0,
      repeatDelay = 0.8,
      reverse = false,
      bidirectional = false,
      pulseLength = 0.3,
      width = 2,
      active = true,
      gap = 4,
      fromOffset,
      toOffset,
      ...props
    },
    forwardedRef,
  ) => {
    const svgRef = React.useRef<SVGSVGElement>(null);
    React.useImperativeHandle(forwardedRef, () => svgRef.current as SVGSVGElement);
    const id = React.useId().replace(/[^a-zA-Z0-9]/g, "");
    const [geometry, setGeometry] = React.useState<Geometry | null>(null);
    const [paused, setPaused] = React.useState(false);

    const fx = fromOffset?.[0] ?? 0;
    const fy = fromOffset?.[1] ?? 0;
    const tx = toOffset?.[0] ?? 0;
    const ty = toOffset?.[1] ?? 0;

    // A passive effect: nodes that render after the beam only have their refs attached once layout effects
    // have run.
    React.useEffect(() => {
      const svg = svgRef.current;
      if (!svg) return;
      let frame = 0;
      const measure = () => {
        frame = 0;
        const a = fromRef.current;
        const b = toRef.current;
        if (!a || !b) return;
        const box = svg.getBoundingClientRect();
        // Inside a scaled parent (a zoomed preview), client rects are scaled but the SVG draws in its own pixels.
        const scale = svg.clientWidth ? box.width / svg.clientWidth : 1;
        const local = (node: Element) => {
          const r = node.getBoundingClientRect();
          const w = r.width / scale;
          const h = r.height / scale;
          return {
            x: (r.left - box.left) / scale + w / 2,
            y: (r.top - box.top) / scale + h / 2,
            hw: w / 2,
            hh: h / 2,
            round: radiusOf(node, w, h) >= Math.min(w, h) * 0.45,
          };
        };
        const A = local(a);
        const B = local(b);
        A.x += fx;
        A.y += fy;
        B.x += tx;
        B.y += ty;
        const dx = B.x - A.x;
        const dy = B.y - A.y;
        const span = Math.hypot(dx, dy);
        if (span < 1) {
          setGeometry(null);
          return;
        }
        const cx = (A.x + B.x) / 2 + (dy / span) * curvature;
        const cy = (A.y + B.y) / 2 - (dx / span) * curvature;
        // Each end leaves its node along the tangent toward the control point.
        const ex = (n: typeof A) => {
          const vx = cx - n.x;
          const vy = cy - n.y;
          const len = Math.hypot(vx, vy) || 1;
          const t = exit(n.hw, n.hh, n.round, vx, vy) + gap;
          return [n.x + (vx / len) * t, n.y + (vy / len) * t];
        };
        const [ax, ay] = ex(A);
        const [bx, by] = ex(B);
        const r = (v: number) => Math.round(v * 10) / 10;
        const d = `M ${r(ax)} ${r(ay)} Q ${r(cx)} ${r(cy)} ${r(bx)} ${r(by)}`;
        setGeometry((prev) => (prev?.d === d ? prev : { d, ax: r(ax), ay: r(ay), bx: r(bx), by: r(by) }));
      };
      const queue = () => {
        if (!frame) frame = requestAnimationFrame(measure);
      };
      measure();
      const container = containerRef?.current ?? svg.parentElement;
      const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(queue);
      [container, fromRef.current, toRef.current].forEach((node) => node && ro?.observe(node));
      window.addEventListener("resize", queue);
      return () => {
        cancelAnimationFrame(frame);
        ro?.disconnect();
        window.removeEventListener("resize", queue);
      };
    }, [containerRef, fromRef, toRef, curvature, gap, fx, fy, tx, ty]);

    // Pulses stop off screen and in background tabs.
    React.useEffect(() => {
      const svg = svgRef.current;
      if (!svg || !active) return;
      let inView = true;
      const sync = () => setPaused(!inView || document.hidden);
      const io =
        typeof IntersectionObserver === "undefined"
          ? null
          : new IntersectionObserver(([entry]) => {
              inView = entry.isIntersecting;
              sync();
            });
      io?.observe(svg);
      document.addEventListener("visibilitychange", sync);
      return () => {
        io?.disconnect();
        document.removeEventListener("visibilitychange", sync);
      };
    }, [active]);

    const L = Math.min(1, Math.max(0.05, pulseLength));
    const cycle = Math.max(0.1, duration) + Math.max(0, repeatDelay);
    const travel = (Math.max(0.1, duration) / cycle) * 100;
    const arrive = (travel * (1 + EDGE)) / (1 + L + 2 * EDGE);
    const fade = Math.min(100, arrive + (0.45 / cycle) * 100);
    const pct = (n: number) => `${Math.round(n * 100) / 100}%`;
    const keyframes = `
@keyframes nk-sb-run-${id} { 0% { stroke-dashoffset: var(--nk-sb-a); } ${pct(travel)}, 100% { stroke-dashoffset: var(--nk-sb-b); } }
@keyframes nk-sb-flash-${id} { 0%, ${pct(Math.max(0, arrive - 0.5))} { opacity: 0; } ${pct(arrive)} { opacity: 1; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); } ${pct(fade)}, 100% { opacity: 0; } }
`;

    const stack = layers(width);
    const pulse = (backward: boolean, offset: number) => {
      const timing = {
        "--nk-sb-name": `nk-sb-run-${id}`,
        "--nk-sb-flash": `nk-sb-flash-${id}`,
        "--nk-sb-cycle": `${cycle}s`,
        "--nk-sb-delay": `${delay + offset}s`,
      } as React.CSSProperties;
      // Forward, a dash of length l with its head at h sits at offset l - h; backward, at -h.
      const still = 0.62;
      return (
        <g key={backward ? "back" : "fore"} style={timing}>
          {stack.map((layer, i) => {
            const l = layer.length * L;
            const vars = backward
              ? { a: -(1 + EDGE), b: L + EDGE, s: -(1 - still) }
              : { a: l + EDGE, b: l - 1 - L - EDGE, s: l - still };
            return (
              <path
                key={i}
                d={geometry?.d}
                pathLength={1}
                fill="none"
                strokeLinecap="round"
                className="nk-signal-beam-pulse"
                style={{
                  stroke: layer.stroke,
                  strokeWidth: layer.width,
                  strokeDasharray: `${l} ${2 + L}`,
                  "--nk-sb-a": vars.a,
                  "--nk-sb-b": vars.b,
                  "--nk-sb-still": vars.s,
                } as React.CSSProperties}
              />
            );
          })}
          {/* The far node catches the light as the pulse lands. */}
          <circle
            cx={backward ? geometry?.ax : geometry?.bx}
            cy={backward ? geometry?.ay : geometry?.by}
            r={width * 7}
            fill={`url(#nk-sb-spill-${id})`}
            className="nk-signal-beam-flash"
          />
        </g>
      );
    };

    const forward = !reverse;

    return (
      <svg
        ref={svgRef}
        aria-hidden
        fill="none"
        data-paused={paused || undefined}
        data-active={active || undefined}
        style={neonVars(color, style)}
        className={cn(
          "[--neon:var(--neon-accent,34_211_238)]",
          "nk-signal-beam pointer-events-none absolute inset-0 size-full overflow-visible",
          className,
        )}
        {...props}
      >
        <style>{css + keyframes}</style>
        <defs>
          <radialGradient id={`nk-sb-spill-${id}`}>
            <stop offset="0" style={{ stopColor: "#fff", stopOpacity: "var(--neon-glow, 1)" }} />
            <stop offset="0.25" style={{ stopColor: "rgb(var(--neon))", stopOpacity: "calc(0.5 * var(--neon-glow, 1))" }} />
            <stop offset="1" style={{ stopColor: "rgb(var(--neon))", stopOpacity: 0 }} />
          </radialGradient>
        </defs>
        {geometry && (
          <>
            {/* The tube: tinted glass when it carries signal, near-black when it's dark. */}
            <path
              d={geometry.d}
              strokeLinecap="round"
              style={{
                stroke: active ? "rgb(var(--neon) / 0.16)" : "rgb(255 255 255 / 0.07)",
                strokeWidth: width,
                strokeDasharray: active ? undefined : `${width * 2} ${width * 2.5}`,
              }}
            />
            {active && pulse(!forward, 0)}
            {active && bidirectional && pulse(forward, cycle / 2)}
          </>
        )}
      </svg>
    );
  },
);
SignalBeam.displayName = "SignalBeam";

export { SignalBeam };
```
