Bevel
Browse the docs

Patch bay

A rack of jacks joined by patch cables: every route from a source to a destination is a cable you can see and pull. For webhook events to handlers, agent tools to agents, and inputs to outputs.

Webhook routing

Patch a billing provider's events to your handlers. One event can feed several handlers.

3 routes · trial.ending, sub.deleted go nowhere

  • invoice.paid to receipt
  • invoice.paid to ledger
  • charge.failed to dunning

  • invoice.paid to receipt
  • invoice.paid to ledger
  • charge.failed to dunning

Installation

Add with the CLI

  1. Once per project, add the Bevel registry to components.json.

    components.json
    {
      "registries": {
        "@bevel": "https://bevel-ui.vercel.app/r/{name}.json"
      }
    }
  2. Add Patch bay. This brings in the Bevel foundation, installs @base-ui/react and cn, and merges the styles into your global CSS.

    Terminal
    npx shadcn@latest add @bevel/patch-bay
Install manually
  1. Install the dependencies.

    Terminal
    pnpm add @base-ui/react cn
  2. Add the Bevel foundation, if you haven't already. Light, depth, materials, motion and shape tokens, plus the utilities components use to apply them.

    styles/bevel.css
    /*
     * Bevel foundation. Every component builds on these tokens and utilities;
     * component stylesheets only add their own palettes and shapes.
     *
     * Layers:
     *   1. Light     one source, above and slightly in front of the screen.
     *   2. Depth     named elevations built from the light: recessed, flush,
     *                raised, pressed, object.
     *   3. Material  a surface recipe (leather, plastic, metal) that reads
     *                --bevel-material and --bevel-material-edge.
     *   4. Motion    two curves, a spring, and durations named by purpose.
     *   5. Shape     radii, stitching, and the type scale for amounts.
     */
    
    :root {
      /* 1. Light */
      --bevel-highlight: oklch(1 0 0 / 0.22);
      --bevel-highlight-strong: oklch(1 0 0 / 0.35);
      --bevel-shade: oklch(0 0 0 / 0.32);
      --bevel-shade-strong: oklch(0 0 0 / 0.5);
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
      /* Stamped into a surface: shaded on the upper inner edge, lit below. */
      --bevel-deboss: 0 -1px 0 oklch(0 0 0 / 0.45), 0 1px 0 oklch(1 0 0 / 0.12);
    
      /* 3. Material defaults; components and variants override these. */
      --bevel-material: oklch(0.47 0.085 52);
      --bevel-material-edge: oklch(0.3 0.06 44);
      --bevel-material-recess: oklch(0.2 0.008 260); /* neutral dark; materials set their own */
      --bevel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .7 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
      --bevel-grain-size: 180px;
    
      /* 2. Depth */
      --bevel-depth-recessed:
        inset 0 1px 2px var(--bevel-shade-strong),
        0 1px 0 var(--bevel-highlight);
      --bevel-depth-flush:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade);
      --bevel-depth-raised:
        inset 0 1px 0 var(--bevel-highlight),
        0 1px 0 var(--bevel-material-edge),
        0 2px 0 var(--bevel-material-edge),
        0 3px 5px var(--bevel-shade);
      --bevel-depth-pressed:
        inset 0 1px 2px var(--bevel-shade),
        0 1px 0 var(--bevel-highlight);
      /* A whole object resting on the page. */
      --bevel-depth-object:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade),
        0 0 0 1px var(--bevel-material-edge),
        0 1px 1px var(--bevel-cast),
        0 6px 12px -2px var(--bevel-cast),
        0 18px 32px -8px var(--bevel-cast);
      /* The shadow under a loose object, for filter: drop-shadow(). */
      --bevel-depth-cast-sm: 0 4px 6px var(--bevel-cast);
      /* A thin object (a card) lying on another surface. */
      --bevel-depth-sheet:
        inset 0 1px 0 var(--bevel-highlight-strong),
        inset 0 0 0 1px oklch(0 0 0 / 0.12),
        0 -1px 2px oklch(0 0 0 / 0.18);
    
      /* 4. Motion */
      --bevel-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
      --bevel-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
      /* Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. */
      --bevel-ease-spring: linear(0, 0.06, 0.202, 0.38, 0.561, 0.723, 0.856, 0.955, 1.023, 1.062, 1.081, 1.083, 1.075, 1.062, 1.047, 1.032, 1.019, 1.009, 1.002, 0.997, 0.994, 0.993, 0.993, 0.994, 1);
      --bevel-duration-press: 100ms; /* a control giving under a finger */
      --bevel-duration-small: 200ms; /* hover lifts, tabs, small state changes */
      --bevel-duration-move: 300ms; /* objects moving: cards, drawers */
      --bevel-duration-ambient: 520ms; /* whole-scene changes: lighting */
      --bevel-stagger: 30ms;
      --bevel-lift: 3px; /* how far an object rises on hover */
      --bevel-perspective: 30rem; /* viewing distance for parts that turn in 3D (covers, flaps) */
      /* Unchosen items in a group sit back in shadow. */
      --bevel-dim: 0.82;
    
      /* 5. Shape */
      --bevel-radius-xs: 0.25rem;
      --bevel-radius-sm: 0.5rem;
      --bevel-radius-md: 0.625rem;
      --bevel-radius-lg: 1rem;
      --bevel-radius-xl: 1.5rem;
      --bevel-stitch-width: 1.5px;
      --bevel-stitch-inset: 0.375rem;
      /* Amounts: the figure, its secondary parts (currency, cents), labels, fine print. */
      --bevel-amount: 2.25rem;
      --bevel-amount-minor: 1.25rem;
      --bevel-label: 0.75rem;
      --bevel-fine: 0.6875rem;
      --bevel-tracking-label: 0.08em; /* stamped labels */
      --bevel-tracking-print: 0.06em; /* printed marks on cards */
    
      /* Paper and ink */
      --bevel-paper: oklch(0.975 0.008 85);
      --bevel-paper-foreground: oklch(0.28 0.02 260);
      --bevel-paper-muted: oklch(0.48 0.015 260);
      --bevel-paper-rule: oklch(0.78 0.04 240 / 0.45);
      --bevel-paper-margin: oklch(0.68 0.14 25 / 0.45);
      --bevel-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9 .35' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E");
      /* Ink soaks unevenly: mostly solid with pale specks. Used as a mask. */
      --bevel-ink-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='i'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 2.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23i)'/%3E%3C/svg%3E");
      --bevel-ink-red: oklch(0.52 0.19 27);
      --bevel-ink-blue: oklch(0.44 0.14 262);
      --bevel-ink-green: oklch(0.5 0.12 155);
      --bevel-ink-black: oklch(0.3 0.01 260);
      --bevel-ink-amber: oklch(0.55 0.13 65);
    
      /* Brass and ivory: instrument hardware and dial faces */
      --bevel-brass: oklch(0.76 0.11 82);
      --bevel-brass-edge: oklch(0.52 0.08 70);
      --bevel-ivory: oklch(0.95 0.025 90);
      --bevel-ivory-foreground: oklch(0.25 0.015 60);
      --bevel-needle: oklch(0.55 0.2 27);
    
      /* Desk stationery: legal pads, pens, pencils, exhibit stickers */
      --bevel-pad: oklch(0.95 0.075 98); /* yellow legal pad */
      --bevel-pad-foreground: oklch(0.27 0.03 255);
      --bevel-pad-rule: oklch(0.72 0.06 235 / 0.45);
      --bevel-pad-margin: oklch(0.62 0.16 25 / 0.55);
      --bevel-pen-red: oklch(0.52 0.2 27); /* correcting pen */
      --bevel-pen-blue: oklch(0.42 0.15 262); /* writing pen */
      --bevel-pencil: oklch(0.55 0.01 260); /* not written in ink yet */
      --bevel-sticker: oklch(0.9 0.12 95); /* exhibit sticker */
      --bevel-sticker-foreground: oklch(0.26 0.03 70);
      /* Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. */
      --bevel-font-marker: "Permanent Marker", "Marker Felt", "Chalkboard SE", ui-rounded, "Arial Rounded MT Bold", system-ui, sans-serif;
    }
    
    .dark {
      --bevel-highlight: oklch(1 0 0 / 0.14);
      /* Paper under dim light, not inverted: it's still paper. */
      --bevel-paper: oklch(0.88 0.008 85);
      --bevel-ivory: oklch(0.86 0.022 90);
      --bevel-pad: oklch(0.86 0.07 98);
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /* Depth */
    @utility bevel-recessed {
      background-color: var(--bevel-material-recess);
      box-shadow: var(--bevel-depth-recessed);
    }
    @utility bevel-flush {
      box-shadow: var(--bevel-depth-flush);
    }
    @utility bevel-raised {
      background-color: color-mix(in oklch, var(--bevel-material), white 7%);
      background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 60%);
      box-shadow: var(--bevel-depth-raised);
    }
    @utility bevel-pressed {
      background-color: color-mix(in oklch, var(--bevel-material), black 4%);
      background-image: none;
      box-shadow: var(--bevel-depth-pressed);
    }
    @utility bevel-object {
      box-shadow: var(--bevel-depth-object);
    }
    
    /* Materials */
    @utility bevel-leather {
      background-color: var(--bevel-material);
      background-image:
        var(--bevel-grain),
        linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade));
      background-size: var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%;
      background-blend-mode: soft-light, normal;
    }
    @utility bevel-plastic {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(115deg, var(--bevel-highlight-strong), transparent 40%),
        linear-gradient(to bottom, transparent 25%, oklch(0 0 0 / 0.45));
      box-shadow: var(--bevel-depth-sheet);
    }
    @utility bevel-metal {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade) 100%),
        repeating-linear-gradient(90deg, oklch(1 0 0 / 0.06) 0 1px, transparent 1px 3px);
      box-shadow:
        inset 0 0 0 1px oklch(0 0 0 / 0.25),
        inset 0 1px 0 var(--bevel-highlight-strong);
    }
    
    /* Details */
    @utility bevel-deboss {
      text-shadow: var(--bevel-deboss);
    }
    @utility bevel-stitch {
      border-color: var(--bevel-stitch, oklch(0.88 0.05 82 / 0.8));
      border-style: dashed;
      border-width: var(--bevel-stitch-width);
      filter: drop-shadow(0 1px 0 var(--bevel-shade));
    }
    /* A lit fill sitting in a recess (meters, progress). Colour from --bevel-fill. */
    @utility bevel-fill {
      background-color: var(--bevel-fill);
      background-image: linear-gradient(to bottom, oklch(1 0 0 / 0.55), transparent 55%, oklch(0 0 0 / 0.18));
      box-shadow: 0 0 6px -1px var(--bevel-fill);
    }
    /* A small indicator light. Colour from --bevel-led. */
    @utility bevel-led {
      background-color: var(--bevel-led);
      background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / 0.85), transparent 45%);
      box-shadow:
        0 0 6px var(--bevel-led),
        inset 0 -1px 1px oklch(0 0 0 / 0.35),
        0 1px 0 var(--bevel-highlight);
    }
    
    /* Paper: a sheet resting on the surface, with fibre in it. */
    @utility bevel-paper {
      background-color: var(--bevel-paper);
      background-image: var(--bevel-fibre);
      background-size: 240px 240px;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
    /* Ink on paper: multiplies into the surface and soaks unevenly. Colour from --bevel-ink. */
    @utility bevel-ink {
      color: var(--bevel-ink, var(--bevel-ink-red));
      border-color: currentColor;
      mix-blend-mode: multiply;
      mask-image: var(--bevel-ink-texture);
      mask-size: 160px 160px;
    }
    /* Glass over an instrument window: one reflection from the light above. Put on an overlay element. */
    @utility bevel-glass {
      pointer-events: none;
      background-image: linear-gradient(165deg, var(--bevel-highlight-strong) 0%, transparent 38%, transparent 70%, var(--bevel-shade) 100%);
    }
    
    /* Material families: one per screen. Set on the screen's container. */
    [data-bevel-family="leather"] {
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
    }
    [data-bevel-family="paper"] {
      --bevel-cast: oklch(0.3 0.01 250 / 0.16);
    }
    .dark :is([data-bevel-family="leather"], [data-bevel-family="paper"]) {
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /*
     * Ruled paper: rules every --bevel-rule-height and a margin line at
     * --bevel-rule-margin. Set --bevel-sheet for the paper colour. The ruling
     * scrolls with the content.
     */
    @utility bevel-ruled {
      background-color: var(--bevel-sheet, var(--bevel-paper));
      background-image:
        linear-gradient(
          to right,
          transparent calc(var(--bevel-rule-margin) - 1px),
          var(--bevel-sheet-margin, var(--bevel-paper-margin)) calc(var(--bevel-rule-margin) - 1px) calc(var(--bevel-rule-margin) + 1px),
          transparent calc(var(--bevel-rule-margin) + 1px)
        ),
        repeating-linear-gradient(
          to bottom,
          transparent 0 calc(var(--bevel-rule-height) - 1px),
          var(--bevel-sheet-rule, var(--bevel-paper-rule)) calc(var(--bevel-rule-height) - 1px) var(--bevel-rule-height)
        ),
        var(--bevel-fibre);
      background-size: 100% 100%, 100% 100%, 240px 240px;
      background-attachment: local;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
  3. Copy patch-bay.tsx into components/ui.

    components/patch-bay.tsx
    "use client"
    
    import * as React from "react"
    import { Button as ButtonPrimitive } from "@base-ui/react/button"
    import { cn } from "cn"
    import { useBevelFeedback } from "@/lib/feedback"
    
    /** One patch: a source jack's value, then a destination jack's value. */
    type PatchBayRoute = [from: string, to: string]
    type PatchBaySide = "source" | "destination"
    
    type Jack = { side: PatchBaySide; value: string; label: string }
    type Point = { x: number; y: number }
    
    type PatchBayContextValue = {
      routes: PatchBayRoute[]
      jacks: Jack[]
      rows: Record<PatchBaySide, string>
      armed: { side: PatchBaySide; value: string } | null
      disabled: boolean
      sockets: React.RefObject<Map<string, HTMLElement>>
      press: (side: PatchBaySide, value: string) => void
      pull: (side: PatchBaySide, value: string) => void
      remove: (route: PatchBayRoute) => void
      register: (jack: Jack, socket: HTMLElement) => () => void
      registerRow: (side: PatchBaySide, label: string) => () => void
      setListed: (listed: boolean) => void
      labelOf: (side: PatchBaySide, value: string) => string
    }
    
    const PatchBayContext = React.createContext<PatchBayContextValue | null>(null)
    
    function usePatchBay() {
      const context = React.useContext(PatchBayContext)
      if (!context) throw new Error("PatchBay parts must be inside <PatchBay>")
      return context
    }
    
    const keyOf = (side: PatchBaySide, value: string) => `${side}:${value}`
    const same = (a: PatchBayRoute, b: PatchBayRoute) => a[0] === b[0] && a[1] === b[1]
    
    function list(names: string[]) {
      if (names.length < 2) return names.join("")
      return `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`
    }
    
    type PatchBayProps = Omit<React.ComponentProps<"div">, "defaultValue" | "onChange"> & {
      /** Current patches, as [source, destination] pairs. */
      value?: PatchBayRoute[]
      defaultValue?: PatchBayRoute[]
      onValueChange?: (value: PatchBayRoute[]) => void
      disabled?: boolean
    }
    
    /**
     * A patch bay: route sources to destinations with cables. Press a jack,
     * then a jack in the other row, to patch them; patch the same pair again to
     * pull the cable. A source can feed many destinations and a destination
     * can take many sources. Put the rows in a PatchBayPanel. Give it an aria-label.
     */
    function PatchBay({
      className,
      value: valueProp,
      defaultValue = [],
      onValueChange,
      disabled = false,
      children,
      ...props
    }: PatchBayProps) {
      const [uncontrolled, setUncontrolled] = React.useState(defaultValue)
      const routes = valueProp ?? uncontrolled
      const [jacks, setJacks] = React.useState<Jack[]>([])
      const [rows, setRows] = React.useState<Record<PatchBaySide, string>>({ source: "sources", destination: "destinations" })
      const [armed, setArmed] = React.useState<PatchBayContextValue["armed"]>(null)
      const [announcement, setAnnouncement] = React.useState("")
      const [listed, setListed] = React.useState(false)
      const sockets = React.useRef(new Map<string, HTMLElement>())
      const feedback = useBevelFeedback()
    
      const labelOf = React.useCallback(
        (side: PatchBaySide, value: string) => jacks.find((j) => j.side === side && j.value === value)?.label ?? value,
        [jacks]
      )
    
      const commit = React.useCallback(
        (next: PatchBayRoute[]) => {
          if (valueProp === undefined) setUncontrolled(next)
          onValueChange?.(next)
        },
        [valueProp, onValueChange]
      )
    
      const remove = React.useCallback(
        (route: PatchBayRoute) => {
          commit(routes.filter((r) => !same(r, route)))
          feedback.click()
          setAnnouncement(`Unpatched ${labelOf("source", route[0])} from ${labelOf("destination", route[1])}.`)
        },
        [commit, routes, feedback, labelOf]
      )
    
      const press = React.useCallback(
        (side: PatchBaySide, value: string) => {
          if (disabled) return
          if (!armed || armed.side === side) {
            if (armed?.value === value) {
              setArmed(null)
              setAnnouncement("Cancelled.")
              return
            }
            setArmed({ side, value })
            feedback.click()
            const other: PatchBaySide = side === "source" ? "destination" : "source"
            setAnnouncement(`Selected ${labelOf(side, value)}. Now choose one of the ${rows[other]}.`)
            return
          }
          const route: PatchBayRoute = side === "destination" ? [armed.value, value] : [value, armed.value]
          setArmed(null)
          if (routes.some((r) => same(r, route))) {
            remove(route)
            return
          }
          commit([...routes, route])
          feedback.thunk()
          setAnnouncement(`Patched ${labelOf("source", route[0])} to ${labelOf("destination", route[1])}.`)
        },
        [disabled, armed, routes, commit, remove, feedback, labelOf, rows]
      )
    
      const pull = React.useCallback(
        (side: PatchBaySide, value: string) => {
          if (disabled) return
          const index = side === "source" ? 0 : 1
          const pulled = routes.filter((r) => r[index] === value)
          if (!pulled.length) return
          commit(routes.filter((r) => r[index] !== value))
          feedback.click()
          setAnnouncement(`Unpatched ${labelOf(side, value)}: ${pulled.length} cable${pulled.length === 1 ? "" : "s"}.`)
        },
        [disabled, routes, commit, feedback, labelOf]
      )
    
      const register = React.useCallback((jack: Jack, socket: HTMLElement) => {
        const key = keyOf(jack.side, jack.value)
        sockets.current.set(key, socket)
        setJacks((all) =>
          all.some((j) => keyOf(j.side, j.value) === key)
            ? all.map((j) => (keyOf(j.side, j.value) === key ? jack : j))
            : [...all, jack]
        )
        return () => {
          sockets.current.delete(key)
          setJacks((all) => all.filter((j) => keyOf(j.side, j.value) !== key))
        }
      }, [])
    
      const registerRow = React.useCallback((side: PatchBaySide, label: string) => {
        setRows((r) => ({ ...r, [side]: label }))
        return () => setRows((r) => ({ ...r, [side]: side === "source" ? "sources" : "destinations" }))
      }, [])
    
      const context = React.useMemo(
        () => ({ routes, jacks, rows, armed, disabled, sockets, press, pull, remove, register, registerRow, setListed, labelOf }),
        [routes, jacks, rows, armed, disabled, press, pull, remove, register, registerRow, labelOf]
      )
    
      return (
        <PatchBayContext value={context}>
          <div
            role="group"
            data-slot="patch-bay"
            data-armed={armed ? "" : undefined}
            data-disabled={disabled ? "" : undefined}
            onKeyDown={(event) => {
              if (event.key === "Escape" && armed) {
                setArmed(null)
                setAnnouncement("Cancelled.")
              }
            }}
            className={cn(
              "flex w-full flex-col gap-4 data-disabled:opacity-60",
              className
            )}
            {...props}
          >
            {children}
            <p role="status" className="sr-only">
              {announcement}
            </p>
            {!listed && (
              <ul aria-label="Routes" className="sr-only">
                {routes.length ? (
                  routes.map((route) => (
                    <li key={route.join("\u0000")}>
                      {labelOf("source", route[0])} to {labelOf("destination", route[1])}
                    </li>
                  ))
                ) : (
                  <li>No routes</li>
                )}
              </ul>
            )}
          </div>
        </PatchBayContext>
      )
    }
    
    /** The rack panel: the rows of jacks, and the cables drawn between them. */
    function PatchBayPanel({ className, children, ...props }: React.ComponentProps<"div">) {
      const { routes, jacks, sockets } = usePatchBay()
      const panel = React.useRef<HTMLDivElement>(null)
      const [points, setPoints] = React.useState<Record<string, Point>>({})
    
      // Where each socket sits, so the cables can be drawn between them.
      const measure = React.useCallback(() => {
        const box = panel.current?.getBoundingClientRect()
        if (!box) return
        const next: Record<string, Point> = {}
        for (const [key, el] of sockets.current) {
          const r = el.getBoundingClientRect()
          next[key] = { x: r.left - box.left + r.width / 2, y: r.top - box.top + r.height / 2 }
        }
        setPoints(next)
      }, [sockets])
      React.useLayoutEffect(measure, [measure, jacks])
      React.useEffect(() => {
        const el = panel.current
        if (!el) return
        const observer = new ResizeObserver(measure)
        observer.observe(el)
        void document.fonts?.ready.then(measure)
        return () => observer.disconnect()
      }, [measure])
    
      const sources = jacks.filter((j) => j.side === "source")
      return (
        <div
          ref={panel}
          data-slot="patch-bay-panel"
          className={cn(
            "bevel-metal bevel-object relative flex w-full flex-col gap-(--patch-bay-row-gap) rounded-(--bevel-radius-md) p-4 text-(--patch-bay-print)",
            className
          )}
          {...props}
        >
          {children}
          <svg aria-hidden data-slot="patch-bay-cables" className="pointer-events-none absolute inset-0 size-full overflow-visible">
            {routes.map((route) => {
              const a = points[keyOf("source", route[0])]
              const b = points[keyOf("destination", route[1])]
              if (!a || !b) return null
              const colour = Math.max(0, sources.findIndex((s) => s.value === route[0])) % 6
              // The cable drops out of the source, hangs, and comes down into the destination.
              const dy = Math.abs(b.y - a.y)
              const span = Math.abs(b.x - a.x)
              const d = `M ${a.x} ${a.y} C ${a.x} ${a.y + dy * 0.9 + span * 0.08}, ${b.x} ${b.y - dy * 0.2}, ${b.x} ${b.y}`
              return (
                <g
                  key={route.join("\u0000")}
                  data-slot="patch-bay-cable"
                  className="transition-opacity duration-(--bevel-duration-small) ease-(--bevel-ease-out) starting:opacity-0 motion-reduce:transition-none"
                  style={{ ["--patch-bay-cable" as string]: `var(--patch-bay-cable-${colour + 1})` }}
                >
                  <path d={d} className="patch-bay-cable-shadow" />
                  <path d={d} className="patch-bay-cable-body" />
                  <circle cx={a.x} cy={a.y} className="patch-bay-plug" />
                  <circle cx={b.x} cy={b.y} className="patch-bay-plug" />
                </g>
              )
            })}
          </svg>
        </div>
      )
    }
    
    const RowContext = React.createContext<PatchBaySide>("source")
    
    type PatchBayRowProps = React.ComponentProps<"div"> & {
      side: PatchBaySide
      /** Names the row, e.g. “Events” or “Handlers”. */
      label: string
    }
    
    /** A row of jacks. Sources sit on top with labels above; destinations below with labels underneath. */
    function PatchBayRow({ className, side, label, children, ...props }: PatchBayRowProps) {
      const { registerRow } = usePatchBay()
      React.useEffect(() => registerRow(side, label), [side, label, registerRow])
      return (
        <RowContext value={side}>
          <div
            role="group"
            aria-label={label}
            data-slot="patch-bay-row"
            data-side={side}
            className={cn("flex flex-col gap-2 data-[side=destination]:flex-col-reverse", className)}
            {...props}
          >
            <span
              aria-hidden
              className="bevel-deboss text-(length:--bevel-fine) font-semibold tracking-(--bevel-tracking-label) uppercase"
            >
              {label}
            </span>
            <div className="flex justify-around gap-1.5">
              {children}
            </div>
          </div>
        </RowContext>
      )
    }
    
    type PatchBayJackProps = Omit<ButtonPrimitive.Props, "value" | "children"> & {
      value: string
      /** The jack's name, printed on its designation strip. */
      children: string
    }
    
    /**
     * One jack: a button named by its label and described by what it's patched
     * to. Press it to pick it up; Delete or Backspace pulls all its cables.
     */
    function PatchBayJack({ className, value, children, onClick, onKeyDown, ...props }: PatchBayJackProps) {
      const { routes, armed, disabled, press, pull, register, labelOf } = usePatchBay()
      const side = React.useContext(RowContext)
      const socket = React.useRef<HTMLSpanElement>(null)
      const descriptionId = React.useId()
      React.useEffect(() => {
        if (!socket.current) return
        return register({ side, value, label: children }, socket.current)
      }, [side, value, children, register])
    
      const index = side === "source" ? 0 : 1
      const mine = routes.filter((r) => r[index] === value)
      const others = mine.map((r) => (side === "source" ? labelOf("destination", r[1]) : labelOf("source", r[0])))
      const isArmed = armed?.side === side && armed.value === value
      const description = others.length
        ? `${side === "source" ? "Patched to" : "Patched from"} ${list(others)}.`
        : "Not patched."
    
      return (
        <span data-slot="patch-bay-position" className="flex max-w-(--patch-bay-jack-width) min-w-0 flex-1 flex-col items-center gap-1.5 in-data-[side=destination]:flex-col-reverse">
          <span
            data-slot="patch-bay-label"
            aria-hidden
            className="bevel-paper flex h-(--patch-bay-label-height) w-full items-center justify-center overflow-hidden rounded-(--bevel-radius-xs) px-0.5 text-center text-(length:--bevel-fine) leading-tight font-semibold [overflow-wrap:anywhere]"
          >
            {/* Let dotted and snake_case names break at their separators. */}
            <span className="line-clamp-2">{children.replace(/([._/])/g, "$1\u200b")}</span>
          </span>
          <ButtonPrimitive
            data-slot="patch-bay-jack"
            data-side={side}
            data-patched={mine.length ? "" : undefined}
            aria-pressed={isArmed}
            aria-describedby={descriptionId}
            disabled={disabled}
            onClick={(event) => {
              onClick?.(event)
              press(side, value)
            }}
            onKeyDown={(event) => {
              onKeyDown?.(event)
              if (event.key === "Delete" || event.key === "Backspace") {
                event.preventDefault()
                pull(side, value)
              }
            }}
            className={cn(
              "group/jack grid size-(--patch-bay-jack-size) cursor-pointer place-items-center rounded-full outline-none",
              "focus-visible:ring-3 focus-visible:ring-(--patch-bay-focus)/70 data-disabled:cursor-not-allowed",
              className
            )}
            {...props}
          >
            <span className="sr-only">{children}</span>
            <span
              ref={socket}
              aria-hidden
              className={cn(
                "patch-bay-socket grid size-full place-items-center rounded-full",
                "transition-[scale,filter] duration-(--bevel-duration-press) ease-(--bevel-ease-out) group-active/jack:scale-95 motion-reduce:transition-none",
                "group-aria-pressed/jack:patch-bay-socket-armed"
              )}
            >
              <span className="patch-bay-hole size-(--patch-bay-hole-size) rounded-full" />
            </span>
          </ButtonPrimitive>
          <span id={descriptionId} hidden>
            {description}
          </span>
        </span>
      )
    }
    
    /** A visible list of patches, each with a button to pull it. Replaces the built-in screen-reader list. */
    function PatchBayRoutes({ className, ...props }: React.ComponentProps<"ul">) {
      const { routes, remove, setListed, labelOf, disabled } = usePatchBay()
      React.useEffect(() => {
        setListed(true)
        return () => setListed(false)
      }, [setListed])
      return (
        <ul aria-label="Routes" data-slot="patch-bay-routes" className={cn("flex flex-col gap-1 text-sm", className)} {...props}>
          {routes.length === 0 && <li className="text-muted-foreground">No routes yet.</li>}
          {routes.map((route) => {
            const from = labelOf("source", route[0])
            const to = labelOf("destination", route[1])
            return (
              <li key={route.join("\u0000")} className="flex items-center justify-between gap-3">
                <span className="min-w-0 truncate">
                  {from} <span aria-hidden>→</span>
                  <span className="sr-only">to</span> {to}
                </span>
                <ButtonPrimitive
                  data-slot="patch-bay-remove"
                  aria-label={`Unpatch ${from} from ${to}`}
                  disabled={disabled}
                  onClick={() => remove(route)}
                  className="shrink-0 rounded-(--bevel-radius-xs) px-2 py-0.5 text-xs font-medium text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring data-disabled:opacity-50"
                >
                  Unpatch
                </ButtonPrimitive>
              </li>
            )
          })}
        </ul>
      )
    }
    
    export { PatchBay, PatchBayPanel, PatchBayRow, PatchBayJack, PatchBayRoutes }
    export type { PatchBayProps, PatchBayRowProps, PatchBayJackProps, PatchBayRoute, PatchBaySide }
  4. Copy patch-bay.css into your styles.

    styles/patch-bay.css
    /*
     * Patch bay: two rows of jacks on a rack panel, joined by patch cables
     * that hang a little between them. Builds on bevel.css (metal, paper,
     * motion).
     */
    
    :root {
      --patch-bay-panel: oklch(0.27 0.008 260); /* black anodised rack panel */
      --patch-bay-panel-edge: oklch(0.17 0.008 260);
      --patch-bay-print: oklch(0.9 0.01 90);
      --patch-bay-ring: oklch(0.78 0.01 260); /* nickel jack bushing */
      --patch-bay-hole: oklch(0.1 0.005 260);
      --patch-bay-armed: oklch(0.82 0.15 85); /* picked up: a warm ring */
      --patch-bay-focus: oklch(0.82 0.15 85);
      --patch-bay-cable-1: oklch(0.6 0.19 27); /* red */
      --patch-bay-cable-2: oklch(0.58 0.14 255); /* blue */
      --patch-bay-cable-3: oklch(0.8 0.15 90); /* yellow */
      --patch-bay-cable-4: oklch(0.6 0.13 150); /* green */
      --patch-bay-cable-5: oklch(0.92 0.01 90); /* white */
      --patch-bay-cable-6: oklch(0.6 0.15 320); /* violet */
      --patch-bay-cable-width: 4px;
      --patch-bay-plug-radius: 7px;
      --patch-bay-jack-size: 2rem;
      --patch-bay-hole-size: 0.75rem;
      --patch-bay-jack-width: 4.5rem;
      --patch-bay-label-height: 1.75rem; /* two lines of fine print */
      --patch-bay-row-gap: 4.5rem; /* room for the cables to hang */
    }
    
    [data-slot="patch-bay-panel"] {
      --bevel-material: var(--patch-bay-panel);
      --bevel-material-edge: var(--patch-bay-panel-edge);
    }
    
    /* A nickel bushing round a dark hole, lit from above. */
    @utility patch-bay-socket {
      background-color: var(--patch-bay-ring);
      background-image: linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 50%, var(--bevel-shade));
      box-shadow:
        0 0 0 2px var(--patch-bay-panel-edge),
        0 2px 3px var(--bevel-shade);
    }
    @utility patch-bay-socket-armed {
      box-shadow:
        0 0 0 2px var(--patch-bay-panel-edge),
        0 0 0 4px var(--patch-bay-armed),
        0 0 10px 2px var(--patch-bay-armed);
    }
    @utility patch-bay-hole {
      background-color: var(--patch-bay-hole);
      box-shadow: var(--bevel-depth-recessed);
    }
    
    /* Cables: a body in the cable's colour over a soft shadow on the panel. */
    @utility patch-bay-cable-body {
      fill: none;
      stroke: var(--patch-bay-cable);
      stroke-width: var(--patch-bay-cable-width);
      stroke-linecap: round;
    }
    @utility patch-bay-cable-shadow {
      fill: none;
      stroke: var(--bevel-shade-strong);
      stroke-width: var(--patch-bay-cable-width);
      stroke-linecap: round;
      translate: 0 3px;
      filter: blur(1.5px);
    }
    @utility patch-bay-plug {
      r: var(--patch-bay-plug-radius);
      fill: var(--patch-bay-cable);
      stroke: var(--bevel-shade-strong);
      stroke-width: 1.5px;
    }
    
    @media (forced-colors: active) {
      [data-slot="patch-bay-jack"] {
        border: 1px solid ButtonText;
      }
      [data-slot="patch-bay-jack"][aria-pressed="true"] {
        outline: 3px solid Highlight;
      }
      .patch-bay-cable-body,
      .patch-bay-plug {
        stroke: CanvasText;
      }
    }
  5. Import the stylesheets after Tailwind in your global CSS, foundation first.

    globals.css
    @import "./bevel.css";
    @import "./patch-bay.css";

Anatomy

Each part is its own component. Leave out the ones you don't need.

Anatomy
<PatchBay aria-label="Webhook routing" value={routes} onValueChange={setRoutes}>
  <PatchBayPanel>
    <PatchBayRow side="source" label="Events">
      <PatchBayJack value="invoice.paid">invoice.paid</PatchBayJack>
    </PatchBayRow>
    <PatchBayRow side="destination" label="Handlers">
      <PatchBayJack value="receipt">Send receipt</PatchBayJack>
    </PatchBayRow>
  </PatchBayPanel>
  <PatchBayRoutes />
</PatchBay>

Parts

PartWhat it does
PatchBaydiv (group)value and defaultValue: [source, destination] pairs. onValueChange, disabled. Announces each patch, and lists the routes for screen readers. Give it an aria-label.
PatchBayPaneldivThe rack panel. Draws the cables, which hang a little between the rows, in a colour per source.
PatchBayRowdiv (group)side source or destination, and a label naming the row. Sources sit on top, destinations below.
PatchBayJackButtonvalue; its text is its name. Press one jack, then one in the other row, to patch them; patch the same pair again to pull it. Escape cancels; Delete pulls all its cables. Described as “Patched to Send receipt and Update ledger.”
PatchBayRoutesulOptional: a visible list of routes with an Unpatch button each. Replaces the built-in screen-reader list.