# Reroute Hero

> A payments-routing hero with the processors on a wheel under your mark. It drills its own outage within two seconds: the live processor drops, the charge reroutes to the next one, then traffic moves back. Visitors can take one offline themselves.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/reroute-hero).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/reroute-hero
- Screenshot: https://shadcn.exchange/shot/keyframeui/reroute-hero/light.png
- Registry item: https://keyframeui.com/r/reroute-hero.json
- Categories: blocks, hero-sections
- Listed: 2026-11-02, source last changed 2026-11-02

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/reroute-hero.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/reroute.json`

## Notes from the registry

Live preview and the components it's built with: https://keyframeui.com/blocks/hero-sections/reroute-hero

## Files

### `registry/blocks/reroute-hero.tsx`

```tsx
"use client";

import * as React from "react";

import { Reroute, type RerouteItem, type RerouteReason } from "@/components/ui/reroute";

/* ── Content ─────────────────────────────────────────────────
 * Everything you'd change to make the section yours: the copy,
 * the processors on the wheel and the outage drill. The drill
 * takes the live processor down, brings it back, then moves
 * traffic on, so a visitor sees a failover within two seconds.
 * It stops the first time they press anything.
 * ─────────────────────────────────────────────────────────── */

/** The one accent: the hub, the cable, the primary buttons. */
const ACCENT = "#ff3b1f";
const ACCENT_DARK = "#ff5a3c";

const BRAND = { name: "Switchyard", href: "#" };
const NAV_LINKS = [
  { label: "Product", href: "#product" },
  { label: "Processors", href: "#processors" },
  { label: "Pricing", href: "#pricing" },
  { label: "Docs", href: "#docs" },
];
const SIGN_IN = { label: "Sign in", href: "#sign-in" };
const NAV_CTA = { label: "Get a demo", href: "#demo" };

const BADGE = { tag: "New", text: "Failover routing is out of beta" };
const HEADLINE = "Checkout that routes around outages.";
const SUBLINE =
  "Switchyard sits between your checkout and every processor you use. When one stalls, the charge retries on the next healthy one before your customer notices.";
const PRIMARY_CTA = { label: "Start routing", href: "#start" };
const SECONDARY_CTA = { label: "Read the docs", href: "#docs" };
const STATS = [
  { value: "99.995%", label: "checkout uptime" },
  { value: "38 ms", label: "median failover" },
  { value: "6", label: "processors, one API" },
];

const PANEL = { title: "Live route", scope: "EU checkout · production", healthy: "All processors up", degraded: "1 processor down" };
const LOG_EMPTY = "Watching every processor…";
/** ms: wait, take the live one down, bring it back, move on. */
const DRILL = { first: 1600, down: 3400, next: 2600, rest: 10000 };

function Mark({ children }: { children: React.ReactNode }) {
  return (
    <svg viewBox="0 0 24 24" className="size-full" aria-hidden>
      {children}
    </svg>
  );
}

// Every processor name and mark is made up.
const PROCESSORS: RerouteItem[] = [
  {
    id: "northpay",
    name: "Northpay",
    detail: "auth 98.4% · 212 ms",
    icon: (
      <Mark>
        <rect x="3" y="3" width="18" height="18" rx="5" fill="#1d4ed8" />
        <path d="M8 16V8l8 8V8" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      </Mark>
    ),
  },
  {
    id: "kestrel",
    name: "Kestrel Payments",
    detail: "auth 97.9% · 188 ms",
    icon: (
      <Mark>
        <path d="M3 16.5 12 5l9 11.5-4.2-2.4L12 19l-4.8-4.9z" fill="#f97316" />
      </Mark>
    ),
  },
  {
    id: "paywell",
    name: "Paywell",
    detail: "auth 98.1% · 240 ms",
    icon: (
      <Mark>
        <circle cx="12" cy="12" r="9" fill="#0d9488" />
        <path d="M6.5 12.5c1.8-2.4 3.6-2.4 5.5 0s3.7 2.4 5.5 0" fill="none" stroke="#fff" strokeWidth="1.9" strokeLinecap="round" />
      </Mark>
    ),
  },
  {
    id: "ledgerline",
    name: "Ledgerline",
    detail: "auth 98.7% · 176 ms",
    icon: (
      <Mark>
        <rect x="4" y="4" width="16" height="4" rx="2" fill="currentColor" />
        <rect x="4" y="10" width="11" height="4" rx="2" fill="currentColor" opacity="0.6" />
        <rect x="4" y="16" width="7" height="4" rx="2" fill="currentColor" opacity="0.35" />
      </Mark>
    ),
  },
  {
    id: "orbit",
    name: "Orbit Pay",
    detail: "auth 97.6% · 205 ms",
    icon: (
      <Mark>
        <circle cx="12" cy="12" r="7.5" fill="none" stroke="#7c3aed" strokeWidth="3" />
        <circle cx="18.2" cy="6.6" r="2.6" fill="#a78bfa" />
      </Mark>
    ),
  },
  {
    id: "tandem",
    name: "Tandem",
    detail: "auth 98.2% · 198 ms",
    icon: (
      <Mark>
        <circle cx="8.5" cy="12" r="5" fill="#16a34a" />
        <circle cx="15.5" cy="12" r="5" fill="#86efac" opacity="0.85" />
      </Mark>
    ),
  },
];

const nameOf = (id: string | null) => PROCESSORS.find((p) => p.id === id)?.name ?? "nothing";
const shortName = (id: string | null) => nameOf(id).split(" ")[0];
const clock = () => new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" });

/* ── Marks ── */

function SwitchyardMark({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" className={className} aria-hidden>
      <rect x="2" y="2" width="20" height="20" rx="6" fill="currentColor" />
      <path d="M9 18V6M9 14.5c0-3.5 6-3 6-8.5" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" />
    </svg>
  );
}

function Arrow({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M3.5 8h9M8.5 4l4 4-4 4" />
    </svg>
  );
}

/* ── Section ── */

type Entry = { id: number; time: string; text: string; fail: boolean };

export function RerouteHero() {
  const [value, setValue] = React.useState(PROCESSORS[0].id);
  const [down, setDown] = React.useState<string[]>([]);
  const [live, setLive] = React.useState<string | null>(PROCESSORS[0].id);
  const [log, setLog] = React.useState<Entry[]>([]);
  const [touched, setTouched] = React.useState(false);
  const liveRef = React.useRef(live);
  const prevLive = React.useRef(live);
  const seq = React.useRef(0);

  const onRoute = React.useCallback((id: string | null, reason: RerouteReason) => {
    const from = prevLive.current;
    prevLive.current = id;
    liveRef.current = id;
    setLive(id);
    const text =
      reason === "failover"
        ? `${shortName(from)} timed out · retried on ${shortName(id)}`
        : reason === "recover"
          ? `${shortName(id)} recovered · traffic moved back`
          : `Routing to ${shortName(id)}`;
    seq.current += 1;
    const entry = { id: seq.current, time: clock(), text, fail: reason === "failover" };
    setLog((l) => [entry, ...l].slice(0, 2));
  }, []);

  // The outage drill, until the visitor takes over.
  React.useEffect(() => {
    if (touched) return;
    let alive = true;
    const timers: number[] = [];
    const wait = (ms: number) => new Promise<void>((r) => timers.push(window.setTimeout(r, ms)));
    (async () => {
      await wait(DRILL.first);
      while (alive) {
        const victim = liveRef.current;
        if (victim) setDown([victim]);
        await wait(DRILL.down);
        if (!alive) return;
        setDown([]);
        await wait(DRILL.next);
        if (!alive) return;
        const at = PROCESSORS.findIndex((p) => p.id === liveRef.current);
        setValue(PROCESSORS[(at + 1) % PROCESSORS.length].id);
        await wait(DRILL.next);
      }
    })();
    return () => {
      alive = false;
      timers.forEach((t) => window.clearTimeout(t));
    };
  }, [touched]);

  const anyDown = down.length > 0;

  return (
    <section
      style={{ "--accent": ACCENT, "--accent-dark": ACCENT_DARK } as React.CSSProperties}
      className="relative overflow-hidden bg-white text-zinc-950 dark:bg-zinc-950 dark:text-white"
    >
      <header className="border-b border-zinc-950/[0.07] dark:border-white/[0.08]">
        <div className="mx-auto flex h-16 max-w-6xl items-center justify-between gap-6 px-6">
          <a
            href={BRAND.href}
            className="flex items-center gap-2 rounded-md text-[15px] font-semibold tracking-tight focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--accent)"
          >
            <SwitchyardMark className="size-6 text-(--accent) dark:text-(--accent-dark)" />
            {BRAND.name}
          </a>
          <nav aria-label="Main" className="hidden items-center gap-8 text-sm text-zinc-600 md:flex dark:text-zinc-400">
            {NAV_LINKS.map((link) => (
              <a
                key={link.label}
                href={link.href}
                className="rounded-sm transition-colors duration-150 hover:text-zinc-950 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--accent) dark:hover:text-white"
              >
                {link.label}
              </a>
            ))}
          </nav>
          <div className="flex items-center gap-2">
            <a
              href={SIGN_IN.href}
              className="hidden h-9 items-center rounded-full bg-zinc-950/[0.04] px-4 text-[13px] font-medium text-zinc-800 transition-colors duration-150 hover:bg-zinc-950/[0.08] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) sm:flex dark:bg-white/[0.06] dark:text-zinc-200 dark:hover:bg-white/[0.1]"
            >
              {SIGN_IN.label}
            </a>
            <a
              href={NAV_CTA.href}
              className="flex h-9 items-center rounded-full bg-[color-mix(in_oklab,var(--accent)_10%,white)] px-4 text-[13px] font-semibold text-(--accent) transition-colors duration-150 hover:bg-[color-mix(in_oklab,var(--accent)_16%,white)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) dark:bg-[color-mix(in_oklab,var(--accent-dark)_16%,transparent)] dark:text-(--accent-dark) dark:hover:bg-[color-mix(in_oklab,var(--accent-dark)_24%,transparent)]"
            >
              {NAV_CTA.label}
            </a>
          </div>
        </div>
      </header>

      <div className="mx-auto grid max-w-6xl grid-cols-1 items-center gap-12 px-6 pb-20 pt-12 sm:pt-16 lg:grid-cols-[1fr_minmax(0,34rem)] lg:gap-14 lg:pb-24 lg:pt-14">
        <div className="max-w-xl">
          <a
            href="#changelog"
            className="inline-flex items-center gap-2 rounded-full border border-zinc-950/[0.08] bg-white py-1 pl-1 pr-3 text-[13px] text-zinc-600 transition-colors duration-150 hover:border-zinc-950/20 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-white/25"
          >
            <span className="rounded-full bg-(--accent) px-2 py-0.5 text-[11px] font-semibold text-white dark:bg-(--accent-dark)">{BADGE.tag}</span>
            {BADGE.text}
            <Arrow className="size-3.5 text-zinc-400" />
          </a>
          <h1 className="mt-6 text-balance text-[2.5rem] font-semibold leading-[1.05] tracking-[-0.04em] sm:text-[3.5rem] lg:text-[3.75rem]">
            {HEADLINE}
          </h1>
          <p className="mt-5 max-w-lg text-pretty text-base leading-relaxed text-zinc-600 sm:text-[17px] dark:text-zinc-400">{SUBLINE}</p>
          <div className="mt-8 flex flex-wrap items-center gap-3">
            <a
              href={PRIMARY_CTA.href}
              className="inline-flex h-11 items-center gap-2 rounded-full bg-(--accent) px-5 text-sm font-semibold text-white shadow-[0_8px_24px_-10px_var(--accent)] transition-[transform,filter] duration-150 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.97] dark:bg-(--accent-dark)"
            >
              {PRIMARY_CTA.label}
              <Arrow className="size-4" />
            </a>
            <a
              href={SECONDARY_CTA.href}
              className="inline-flex h-11 items-center rounded-full border border-zinc-950/10 px-5 text-sm font-semibold text-zinc-900 transition-colors duration-150 hover:border-zinc-950/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) dark:border-white/15 dark:text-white dark:hover:border-white/30"
            >
              {SECONDARY_CTA.label}
            </a>
          </div>
          <dl className="mt-12 grid max-w-md grid-cols-3 gap-6 border-t border-zinc-950/[0.07] pt-6 dark:border-white/[0.08]">
            {STATS.map((s) => (
              <div key={s.label}>
                <dt className="sr-only">{s.label}</dt>
                <dd className="text-xl font-semibold tabular-nums tracking-tight sm:text-2xl">{s.value}</dd>
                <dd className="mt-1 text-xs leading-snug text-zinc-500 dark:text-zinc-400">{s.label}</dd>
              </div>
            ))}
          </dl>
        </div>

        <div className="relative">
          <div
            aria-hidden
            className="pointer-events-none absolute -inset-x-10 -inset-y-12 -z-0 bg-[radial-gradient(closest-side,color-mix(in_oklab,var(--accent)_13%,transparent),transparent)]"
          />
          <div className="relative overflow-hidden rounded-[28px] border border-zinc-950/[0.08] bg-white shadow-[0_1px_2px_rgba(0,0,0,0.04),0_24px_60px_-28px_rgba(0,0,0,0.18)] dark:border-white/10 dark:bg-zinc-900 dark:shadow-none">
            <div className="flex items-center justify-between gap-3 px-5 pt-5">
              <div className="min-w-0">
                <p className="text-[13px] font-medium tracking-tight">{PANEL.title}</p>
                <p className="truncate text-xs text-zinc-500 dark:text-zinc-400">{PANEL.scope}</p>
              </div>
              <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-zinc-950/[0.08] px-2.5 py-1 text-[11px] font-medium text-zinc-600 dark:border-white/10 dark:text-zinc-300">
                <span className={anyDown ? "size-1.5 rounded-full bg-zinc-950 dark:bg-zinc-200" : "size-1.5 rounded-full bg-(--accent) dark:bg-(--accent-dark)"} />
                <span aria-live="polite">{anyDown ? PANEL.degraded : PANEL.healthy}</span>
              </span>
            </div>
            <Reroute
              className="mt-1 [--reroute-accent:var(--accent)] dark:[--reroute-accent:var(--accent-dark)]"
              label="Payment processor"
              items={PROCESSORS}
              value={value}
              onValueChange={(id) => {
                setTouched(true);
                setValue(id);
              }}
              down={down}
              onRoute={onRoute}
              hub={<SwitchyardGlyph />}
            />
            <div className="flex flex-col items-start gap-3 border-t border-zinc-950/[0.06] px-5 py-4 sm:flex-row sm:items-center sm:justify-between dark:border-white/[0.08]">
              <ol className="w-full min-w-0 flex-1 space-y-1" aria-label="Routing log">
                {log.length === 0 ? (
                  <li className="text-xs text-zinc-400 dark:text-zinc-500">{LOG_EMPTY}</li>
                ) : (
                  log.map((e, i) => (
                    <li key={e.id} className={i ? "flex gap-2 text-xs opacity-50" : "flex gap-2 text-xs"}>
                      <span className="shrink-0 font-mono tabular-nums text-zinc-400 dark:text-zinc-500">{e.time}</span>
                      <span className={e.fail ? "truncate font-medium" : "truncate text-zinc-600 dark:text-zinc-300"}>{e.text}</span>
                    </li>
                  ))
                )}
              </ol>
              <button
                type="button"
                onClick={() => {
                  setTouched(true);
                  setDown(anyDown ? [] : live ? [live] : []);
                }}
                className="h-9 shrink-0 rounded-full bg-zinc-950 px-4 text-[13px] font-medium text-white transition-transform duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.97] dark:bg-white dark:text-zinc-950"
              >
                {anyDown ? `Bring ${shortName(down[0])} back` : `Take ${shortName(live)} offline`}
              </button>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

function SwitchyardGlyph() {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" aria-hidden>
      <path d="M8.5 20V4M8.5 15c0-4.5 7-4 7-11" />
    </svg>
  );
}
```
