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