# Reroute: Model Failover

> An inference gateway over six model providers that drills its own outages: the live provider drops, the wheel reroutes, and it turns back once the provider recovers. Take one down yourself.

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-demo).

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

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/reroute-demo.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, examples and props: https://keyframeui.com/components/reroute#reroute-demo

## Files

### `registry/demos/reroute-demo.tsx`

```tsx
"use client";

import * as React from "react";

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

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

// Fernwood's inference gateway and the model providers it routes between. Every name and mark is made up.
const PROVIDERS: RerouteItem[] = [
  {
    id: "halcyon",
    name: "Halcyon 3 Turbo",
    detail: "p50 182 ms · us-east",
    icon: (
      <Mark>
        <circle cx="12" cy="12" r="9" fill="#2563eb" />
        <path d="M6.5 13.2c1.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: "okto",
    name: "Okto Large",
    detail: "p50 211 ms · eu-west",
    icon: (
      <Mark>
        <path d="M8.3 3h7.4L21 8.3v7.4L15.7 21H8.3L3 15.7V8.3z" fill="currentColor" />
        <circle cx="12" cy="12" r="3.2" fill="none" strokeWidth="1.8" className="stroke-white dark:stroke-zinc-900" />
      </Mark>
    ),
  },
  {
    id: "brightline",
    name: "Brightline 2",
    detail: "p50 164 ms · us-west",
    icon: (
      <Mark>
        <circle cx="12" cy="12" r="4.6" fill="#f59e0b" />
        <g stroke="#f59e0b" strokeWidth="2" strokeLinecap="round">
          <path d="M12 2.5v2.4M12 19.1v2.4M2.5 12h2.4M19.1 12h2.4M5.3 5.3l1.7 1.7M17 17l1.7 1.7M5.3 18.7 7 17M17 7l1.7-1.7" />
        </g>
      </Mark>
    ),
  },
  {
    id: "vesper",
    name: "Vesper Mini",
    detail: "p50 96 ms · us-east",
    icon: (
      <Mark>
        <path d="M15.8 3.4A9 9 0 1 0 20.6 15 7 7 0 0 1 15.8 3.4z" fill="#7c3aed" />
        <circle cx="17.6" cy="6.4" r="1.4" fill="#c4b5fd" />
      </Mark>
    ),
  },
  {
    id: "corvid",
    name: "Corvid R1",
    detail: "p50 240 ms · ap-south",
    icon: (
      <Mark>
        <path d="M4 18 12 4l8 14z" fill="#0f766e" />
        <path d="M9.2 14.6h5.6" stroke="#5eead4" strokeWidth="1.8" strokeLinecap="round" />
      </Mark>
    ),
  },
  {
    id: "lumen",
    name: "Lumen Edge",
    detail: "p50 128 ms · eu-central",
    icon: (
      <Mark>
        <path d="M12 2.8c.9 4.9 4.3 8.3 9.2 9.2-4.9.9-8.3 4.3-9.2 9.2-.9-4.9-4.3-8.3-9.2-9.2 4.9-.9 8.3-4.3 9.2-9.2z" fill="#e11d48" />
      </Mark>
    ),
  },
];

type Entry = { id: number; time: string; text: string; tone: "fail" | "ok" };

const clock = () =>
  new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" });

// The outage drill: the live provider drops, comes back, then traffic moves on.
const DRILL = { down: 2400, recover: 3200, next: 2600, rest: 9000 }; // ms; `rest` is how long the drill waits after you take over

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

  const name = (id: string | null) => PROVIDERS.find((p) => p.id === id)?.name ?? "nothing";

  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"
        ? `${name(from)} down · rerouted to ${name(id)}`
        : reason === "recover"
          ? `${name(id)} back · traffic returned`
          : `Routing to ${name(id)}`;
    seq.current += 1;
    const entry: Entry = { id: seq.current, time: clock(), text, tone: reason === "failover" ? "fail" : "ok" };
    setLog((l) => [entry, ...l].slice(0, 3));
  }, []);

  // The drill runs on its own until you press something, then waits a while.
  React.useEffect(() => {
    let alive = true;
    const timers: number[] = [];
    const wait = (ms: number) => new Promise<void>((r) => timers.push(window.setTimeout(r, ms)));
    const run = async () => {
      if (touched) await wait(DRILL.rest);
      while (alive) {
        await wait(DRILL.down);
        if (!alive) return;
        const victim = liveRef.current;
        if (victim) setDown([victim]);
        await wait(DRILL.recover);
        if (!alive) return;
        setDown([]);
        await wait(DRILL.next);
        if (!alive) return;
        const at = PROVIDERS.findIndex((p) => p.id === liveRef.current);
        setValue(PROVIDERS[(at + 1) % PROVIDERS.length].id);
      }
    };
    run();
    return () => {
      alive = false;
      timers.forEach((t) => window.clearTimeout(t));
    };
  }, [touched]);

  const anyDown = down.length > 0;

  return (
    <div className="w-full max-w-xl overflow-hidden rounded-3xl border border-zinc-950/[0.08] bg-white bg-[radial-gradient(120%_55%_at_50%_0%,rgba(255,59,31,0.07),transparent)] shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:border-white/10 dark:bg-zinc-950 dark:bg-[radial-gradient(120%_55%_at_50%_0%,rgba(255,90,60,0.1),transparent)]">
      <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 text-zinc-950 dark:text-white">Fernwood gateway</p>
          <p className="text-xs text-zinc-500 dark:text-zinc-400">One endpoint, six model providers</p>
        </div>
        <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-zinc-950/[0.08] bg-white px-2.5 py-1 text-[11px] font-medium text-zinc-600 dark:border-white/10 dark:bg-zinc-900 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-[#ff3b1f]"} />
          {anyDown ? "1 provider down" : "All providers up"}
        </span>
      </div>

      <Reroute
        className="mt-2"
        label="Model provider"
        items={PROVIDERS}
        value={value}
        onValueChange={(id) => {
          setValue(id);
          setTouched((t) => t + 1);
        }}
        down={down}
        onRoute={onRoute}
        hub={
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
            <path d="M5 7h9a4 4 0 0 1 0 8H9" />
            <path d="M8 12l-3 3 3 3" />
          </svg>
        }
      />

      <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">Watching for outages…</li>
          ) : (
            log.map((e, i) => (
              <li
                key={e.id}
                className="flex items-center gap-2 text-xs transition-opacity duration-300"
                style={{ opacity: 1 - i * 0.3 }}
              >
                <span className="font-mono tabular-nums text-zinc-400 dark:text-zinc-500">{e.time}</span>
                <span
                  className={
                    e.tone === "fail"
                      ? "truncate font-medium text-zinc-950 dark:text-white"
                      : "truncate text-zinc-600 dark:text-zinc-300"
                  }
                >
                  {e.text}
                </span>
              </li>
            ))
          )}
        </ol>
        <button
          type="button"
          onClick={() => {
            setTouched((t) => t + 1);
            setDown(anyDown ? [] : live ? [live] : []);
          }}
          className="h-9 shrink-0 rounded-full bg-zinc-950 px-4 text-[13px] font-medium text-white outline-offset-2 outline-[#ff3b1f] transition-transform duration-150 focus-visible:outline-2 active:scale-[0.97] dark:bg-white dark:text-zinc-950"
        >
          {anyDown ? `Restore ${name(down[0]).split(" ")[0]}` : `Take ${name(live).split(" ")[0]} down`}
        </button>
      </div>
    </div>
  );
}
```
