# Reroute Features

> A bento grid of platform features: a processor wheel that reroutes around its own outages, a charge snippet, regions, a vault with its tokens drifting round a glowing lock, payment methods, flat pricing rows, and refunds clearing a Checkpoint of finance rules.

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

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

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/reroute-features.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/checkpoint.json`, `https://keyframeui.com/r/reroute.json`, `utils`

## Notes from the registry

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

## Files

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

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { Checkpoint, type CheckpointRequest, type CheckpointRule } from "@/components/ui/checkpoint";
import { Reroute, type RerouteItem } from "@/components/ui/reroute";

/* ── Content ─────────────────────────────────────────────────
 * Everything you'd change to make the section yours: the copy
 * for each cell, the processors on the wheel and the outage it
 * drills, the snippet, the vault's tokens, the payment methods,
 * the pricing rows, and the rules a refund has to clear.
 * ─────────────────────────────────────────────────────────── */

/** The one accent: the hub, the cable, the checks and highlighted values. */
const ACCENT = "#ff3b1f";
const ACCENT_DARK = "#ff5a3c";

const EYEBROW = "Platform";
const HEADLINE = "Every charge finds a way through.";
const SUBLINE = "Routing, retries and approvals in one layer between your checkout and your processors.";

const ROUTING = {
  title: "Route around any outage",
  body: "When a processor stalls, the charge retries on the next healthy one in under 50 ms. Customers never see the error.",
};
/** ms: how long a processor stays down in the drill, and the calm between drills. */
const DRILL = { first: 2200, down: 2600, calm: 4200 };

const API = {
  title: "One API for every processor",
  body: "Keep the contracts you already have. Switchyard speaks to each processor so your code doesn't have to.",
  // Kept as lines, not one template string, so no line is read as an import.
  code: [
    ["await", " yard.charges.create({", "", ""],
    ["", "  amount: ", "", "4200,"],
    ["", "  currency: ", "", '"eur",'],
    ["", "  route: ", "", '"best-approval",'],
    ["", "});", "", ""],
  ],
};

const REGIONS = {
  title: "Runs in the region you pick",
  body: "Deploy in the US, the EU or Asia Pacific, and keep card data where your auditors want it.",
  chips: ["us-east", "eu-central", "ap-southeast"],
};

const VAULT = {
  title: "Card data stays in your vault",
  body: "Tokens live in your own cloud account, so you can switch processors without moving a single card.",
  chips: ["•••• 4242", "tok_9fQ2", "exp 08/29", "3DS ✓", "fp_a71c"],
};

const METHODS = {
  stat: "140+",
  unit: "ways to pay",
  title: "Local methods in 60 markets",
  body: "Wallets, bank debits and instant transfers switch on per country, with the right one shown at checkout.",
  rows: [
    ["Cards", "Wallets", "SEPA Debit", "Pix", "UPI", "ACH", "Bank transfer"],
    ["Pay by link", "BACS", "Installments", "Instant payouts", "BLIK", "FPX"],
    ["Network tokens", "3-D Secure", "Subscriptions", "Multi-currency", "PayNow", "Boleto"],
  ],
};

const PRICING = {
  stat: "0 bps",
  unit: "on your volume",
  title: "A flat fee, never a cut",
  body: "One monthly price per environment. Retries, failover and tokens are included, however much you process.",
  rows: [
    { label: "Smart retries", value: "Included" },
    { label: "Failover routing", value: "Included" },
    { label: "Network tokens", value: "Included" },
  ],
};

const APPROVALS = {
  title: "Rules finance signs off on",
  body: "Refunds and payouts clear your policy first. Anything unusual waits for a second pair of eyes, and every decision is logged.",
};

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%",
    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",
    detail: "auth 97.9%",
    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%",
    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%",
    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%",
    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>
    ),
  },
];

const RULES: CheckpointRule[] = [
  { id: "role", label: "Requested by", detail: "Support or finance", value: "SUPPORT" },
  { id: "amount", label: "Amount", detail: "Over $500 needs sign-off", value: "≤ $500" },
  { id: "destination", label: "Destination", detail: "Back to the card it came from", value: "ORIGINAL" },
  { id: "age", label: "Charge age", detail: "Within the refund window", value: "≤ 120 DAYS" },
];

const REQUESTS: CheckpointRequest[] = [
  { id: "r1", label: "refund ch_3Lq8", meta: "$64.00 · Ana, support" },
  { id: "r2", label: "refund ch_9Pe2", meta: "$1,240.00 · Ana, support", verdicts: { amount: "hold" } },
  {
    id: "r3",
    label: "refund ch_2Hx7",
    meta: "$310.00 to a new card",
    verdicts: { destination: "block" },
    reason: "Refunds go back to the original card",
  },
];

/* ── Bits ── */

const REDUCED = "(prefers-reduced-motion: reduce)";

function useReducedMotion() {
  return React.useSyncExternalStore(
    (onChange) => {
      const q = window.matchMedia(REDUCED);
      q.addEventListener("change", onChange);
      return () => q.removeEventListener("change", onChange);
    },
    () => window.matchMedia(REDUCED).matches,
    () => false,
  );
}

function Check({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
      <path d="M3.5 8.5 6.5 11.5 12.5 5" />
    </svg>
  );
}

function Cell({
  title,
  body,
  className,
  children,
  bare,
  center,
}: {
  title: string;
  body: string;
  className?: string;
  children: React.ReactNode;
  bare?: boolean;
  /** Center the visual in the space above the caption (the diagram cells). */
  center?: boolean;
}) {
  return (
    <article
      data-cell
      className={cn(
        "group relative flex flex-col overflow-hidden rounded-2xl border border-zinc-950/[0.08] bg-white transition-[border-color] duration-200 [@media(hover:hover)]:hover:border-zinc-950/[0.16]",
        "bg-[linear-gradient(to_bottom,color-mix(in_oklab,var(--accent)_5%,transparent),transparent_42%)]",
        "dark:border-white/[0.08] dark:bg-zinc-900/60 dark:[@media(hover:hover)]:hover:border-white/[0.16]",
        className,
      )}
    >
      <div className={cn("relative flex-1", !bare && "px-5 pt-5", center && "flex flex-col justify-center")}>{children}</div>
      <div className="px-5 pb-5 pt-4">
        <h3 className="text-[15px] font-medium tracking-tight text-zinc-950 dark:text-white">{title}</h3>
        <p className="mt-1.5 max-w-md text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">{body}</p>
      </div>
    </article>
  );
}

/** Chips floating round the vault's glowing lock. They drift a couple of px, slowly, and stop for reduced motion. */
function Vault({ reduced }: { reduced: boolean }) {
  const chips = React.useRef<(HTMLSpanElement | null)[]>([]);
  const glow = React.useRef<HTMLSpanElement>(null);
  React.useEffect(() => {
    if (reduced) return;
    const anims = chips.current.map((el, i) =>
      el?.animate([{ transform: "translateY(0)" }, { transform: `translateY(${i % 2 ? 3 : -3}px)` }], {
        duration: 2600 + i * 380,
        direction: "alternate",
        iterations: Infinity,
        easing: "ease-in-out",
        delay: -i * 600,
      }),
    );
    const g = glow.current?.animate([{ opacity: 0.55 }, { opacity: 1 }], {
      duration: 2000,
      direction: "alternate",
      iterations: Infinity,
      easing: "ease-in-out",
    });
    return () => {
      anims.forEach((a) => a?.cancel());
      g?.cancel();
    };
  }, [reduced]);

  // A 352 × 236 box, scaled to fit: the lock sits low in the middle and the
  // five chips ride a circle of radius 150 round it, so the arc runs through them.
  const spots = [
    { x: 13, y: 44.5 },
    { x: 27.4, y: 22.5 },
    { x: 50, y: 12.7 },
    { x: 72.6, y: 22.5 },
    { x: 87, y: 44.5 },
  ];
  return (
    <div aria-hidden className="relative mx-auto mt-2 aspect-[352/236] w-full max-w-[22rem] overflow-hidden [mask-image:linear-gradient(to_bottom,#000_62%,transparent)]">
      <span className="absolute left-1/2 top-[76.3%] aspect-square w-[85.2%] -translate-x-1/2 -translate-y-1/2 rounded-full border border-zinc-950/[0.08] dark:border-white/[0.09]" />
      <span className="absolute left-1/2 top-[76.3%] aspect-square w-[54.5%] -translate-x-1/2 -translate-y-1/2 rounded-full border border-dashed border-zinc-950/[0.07] dark:border-white/[0.08]" />
      {VAULT.chips.map((chip, i) => (
        <span
          key={chip}
          ref={(el) => {
            chips.current[i] = el;
          }}
          className="absolute will-change-transform"
          style={{ left: `${spots[i].x}%`, top: `${spots[i].y}%` }}
        >
          <span className="block -translate-x-1/2 -translate-y-1/2 whitespace-nowrap rounded-lg border border-[color-mix(in_oklab,var(--accent)_22%,transparent)] bg-[color-mix(in_oklab,var(--accent)_7%,white)] px-2 py-1 font-mono text-[11px] text-(--accent) shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:bg-[color-mix(in_oklab,var(--accent-dark)_14%,#18181b)] dark:text-(--accent-dark)">
            {chip}
          </span>
        </span>
      ))}
      <span
        ref={glow}
        className="absolute left-1/2 top-[76.3%] aspect-square w-[46%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[radial-gradient(closest-side,color-mix(in_oklab,var(--accent)_38%,transparent),transparent)] will-change-[opacity]"
      />
      <span className="absolute left-1/2 top-[76.3%] grid size-14 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-2xl bg-[linear-gradient(180deg,color-mix(in_oklab,var(--accent),white_16%),var(--accent))] text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.35),0_10px_24px_-8px_color-mix(in_oklab,var(--accent)_70%,transparent)]">
        <svg viewBox="0 0 24 24" className="size-6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="5" y="10.5" width="14" height="10" rx="2.5" />
          <path d="M8.5 10.5V8a3.5 3.5 0 0 1 7 0v2.5" />
          <circle cx="12" cy="15.5" r="1.3" fill="currentColor" stroke="none" />
        </svg>
      </span>
    </div>
  );
}

/* ── Section ── */

export function RerouteFeatures() {
  const reduced = useReducedMotion();
  const [down, setDown] = React.useState<string[]>([]);
  const live = React.useRef<string | null>(PROCESSORS[0].id);
  const sectionRef = React.useRef<HTMLElement>(null);

  // The routing cell drills an outage now and then, while it's on screen.
  React.useEffect(() => {
    const section = sectionRef.current;
    if (!section) return;
    let visible = false;
    let timer = 0;
    const next = (ms: number, fn: () => void) => {
      window.clearTimeout(timer);
      timer = window.setTimeout(fn, ms);
    };
    const drop = () => {
      if (!visible) return;
      if (live.current) setDown([live.current]);
      next(DRILL.down, () => {
        setDown([]);
        next(DRILL.calm, drop);
      });
    };
    const io = new IntersectionObserver(([entry]) => {
      const was = visible;
      visible = entry.isIntersecting;
      if (visible && !was) next(DRILL.first, drop);
      if (!visible) {
        window.clearTimeout(timer);
        setDown([]);
      }
    });
    io.observe(section);
    return () => {
      io.disconnect();
      window.clearTimeout(timer);
    };
  }, []);

  // Cells rise in, one after another, if the section starts below the fold.
  React.useEffect(() => {
    const section = sectionRef.current;
    if (!section || reduced) return;
    if (section.getBoundingClientRect().top < window.innerHeight * 0.9) return;
    const cells = Array.from(section.querySelectorAll<HTMLElement>("[data-cell]"));
    cells.forEach((c) => (c.style.opacity = "0"));
    const io = new IntersectionObserver(
      ([entry]) => {
        if (!entry.isIntersecting) return;
        io.disconnect();
        cells.forEach((c, i) => {
          c.style.opacity = "";
          c.animate(
            [
              { opacity: 0, transform: "translateY(14px)" },
              { opacity: 1, transform: "translateY(0)" },
            ],
            { duration: 520, delay: i * 60, easing: "cubic-bezier(0.23, 1, 0.32, 1)", fill: "backwards" },
          );
        });
      },
      { threshold: 0.12 },
    );
    io.observe(section);
    return () => {
      io.disconnect();
      cells.forEach((c) => (c.style.opacity = ""));
    };
  }, [reduced]);

  return (
    <section
      ref={sectionRef}
      style={{ "--accent": ACCENT, "--accent-dark": ACCENT_DARK } as React.CSSProperties}
      className="bg-white text-zinc-950 dark:bg-zinc-950 dark:text-white"
    >
      <div className="mx-auto max-w-6xl px-6 py-20 sm:py-24">
        <div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
          <div>
            <p className="text-[13px] font-medium text-(--accent) dark:text-(--accent-dark)">{EYEBROW}</p>
            <h2 className="mt-2 text-balance text-[2rem] font-medium leading-[1.1] tracking-[-0.035em] sm:text-[2.75rem]">{HEADLINE}</h2>
          </div>
          <p className="max-w-sm text-pretty text-[15px] leading-relaxed text-zinc-500 dark:text-zinc-400">{SUBLINE}</p>
        </div>

        <div className="mt-10 grid grid-cols-1 gap-3 md:grid-cols-2 lg:mt-12 lg:grid-cols-12">
          {/* Routing */}
          <Cell title={ROUTING.title} body={ROUTING.body} className="lg:col-span-4" bare center>
            <Reroute
              className="pt-5 [--reroute-accent:var(--accent)] [--reroute-height:clamp(14rem,58cqw,16.5rem)] dark:[--reroute-accent:var(--accent-dark)]"
              label="Payment processor"
              items={PROCESSORS}
              defaultValue="northpay"
              down={down}
              onRoute={(id) => (live.current = id)}
              hub={
                <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>
              }
            />
          </Cell>

          {/* API + regions, stacked */}
          <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:col-span-2 md:grid-cols-2 lg:col-span-4 lg:grid-cols-1">
            <Cell title={API.title} body={API.body}>
              <pre
                aria-label="Creating a charge"
                className="overflow-hidden rounded-xl border border-zinc-950/[0.06] bg-[color-mix(in_oklab,var(--accent)_4%,white)] px-3.5 py-3 font-mono text-[11.5px] leading-[1.7] text-zinc-700 dark:border-white/[0.06] dark:bg-white/[0.03] dark:text-zinc-300"
              >
                {API.code.map(([kw, a, kw2, b], i) => (
                  <div key={i} className="whitespace-pre">
                    <span className="text-zinc-950 dark:text-white">{kw}</span>
                    {a}
                    <span className="text-zinc-950 dark:text-white">{kw2}</span>
                    <span className={/^["\d]/.test(b) ? "text-(--accent) dark:text-(--accent-dark)" : undefined}>{b}</span>
                  </div>
                ))}
              </pre>
            </Cell>
            <Cell title={REGIONS.title} body={REGIONS.body}>
              <div className="flex flex-wrap gap-1.5">
                {REGIONS.chips.map((r, i) => (
                  <span
                    key={r}
                    className={cn(
                      "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 font-mono text-[11px]",
                      i === 1
                        ? "border-[color-mix(in_oklab,var(--accent)_30%,transparent)] bg-[color-mix(in_oklab,var(--accent)_8%,white)] text-(--accent) dark:bg-[color-mix(in_oklab,var(--accent-dark)_14%,transparent)] dark:text-(--accent-dark)"
                        : "border-zinc-950/[0.08] text-zinc-600 dark:border-white/10 dark:text-zinc-400",
                    )}
                  >
                    <span className={cn("size-1.5 rounded-full", i === 1 ? "bg-current" : "bg-zinc-300 dark:bg-zinc-600")} />
                    {r}
                  </span>
                ))}
              </div>
            </Cell>
          </div>

          {/* Vault */}
          <Cell title={VAULT.title} body={VAULT.body} className="lg:col-span-4" bare center>
            <Vault reduced={reduced} />
          </Cell>

          {/* Methods + pricing, stacked */}
          <div className="grid grid-cols-1 gap-3 md:col-span-2 lg:col-span-6">
            <Cell title={METHODS.title} body={METHODS.body} bare>
              <div className="flex items-center gap-2 pt-5">
                <div className="shrink-0 pl-5">
                  <p className="text-[2.25rem] font-medium leading-none tracking-[-0.04em] tabular-nums">{METHODS.stat}</p>
                  <p className="mt-1.5 text-[15px] text-zinc-500 dark:text-zinc-400">{METHODS.unit}</p>
                </div>
                <div
                  aria-label="Payment methods"
                  className="min-w-0 flex-1 space-y-1.5 overflow-hidden py-1 [mask-image:linear-gradient(to_right,transparent,#000_18%,#000_88%,transparent)]"
                >
                  {METHODS.rows.map((row, r) => (
                    <div key={r} className="flex gap-1.5" style={{ marginLeft: `${[0, -28, -12][r]}px` }}>
                      {row.map((m) => (
                        <span
                          key={m}
                          className="shrink-0 rounded-lg border border-zinc-950/[0.08] bg-white px-3 py-1.5 text-xs text-zinc-600 dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-400"
                        >
                          {m}
                        </span>
                      ))}
                    </div>
                  ))}
                </div>
              </div>
            </Cell>
            <Cell title={PRICING.title} body={PRICING.body} bare>
              <div className="flex flex-col gap-4 px-5 pt-5 sm:flex-row sm:items-center sm:gap-6">
                <div className="shrink-0">
                  <p className="text-[2.25rem] font-medium leading-none tracking-[-0.04em] tabular-nums">{PRICING.stat}</p>
                  <p className="mt-1.5 text-[15px] text-zinc-500 dark:text-zinc-400">{PRICING.unit}</p>
                </div>
                <dl className="min-w-0 flex-1 divide-y divide-zinc-950/[0.06] rounded-xl border border-zinc-950/[0.08] bg-white px-3.5 dark:divide-white/[0.06] dark:border-white/10 dark:bg-zinc-900">
                  {PRICING.rows.map((row) => (
                    <div key={row.label} className="flex items-center justify-between gap-3 py-2.5">
                      <dt className="text-[13px] text-zinc-700 dark:text-zinc-300">{row.label}</dt>
                      <dd className="flex items-center gap-2 text-[13px] font-medium text-(--accent) dark:text-(--accent-dark)">
                        {row.value}
                        <span className="grid size-[18px] place-items-center rounded-[5px] bg-(--accent) text-white dark:bg-(--accent-dark)">
                          <Check className="size-3" />
                        </span>
                      </dd>
                    </div>
                  ))}
                </dl>
              </div>
            </Cell>
          </div>

          {/* Approvals */}
          <Cell title={APPROVALS.title} body={APPROVALS.body} className="md:col-span-2 lg:col-span-6">
            <Checkpoint
              className="[--checkpoint-accent:var(--accent)] dark:[--checkpoint-accent:var(--accent-dark)]"
              rules={RULES}
              requests={REQUESTS}
              approver="Priya, finance"
            />
          </Cell>
        </div>
      </div>
    </section>
  );
}
```
