{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "webhook-relay",
  "type": "registry:component",
  "title": "Webhook Relay",
  "description": "Signed event packets fan out from a relay to three endpoints; one fails, bounces back and retries with a growing backoff until it lands. Hover a node to pause and read it.",
  "categories": [
    "illustration",
    "Connections"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/webhook-relay.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { type LucideIcon, MailIcon, ServerIcon, TruckIcon, WalletIcon, WebhookIcon } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  type MotionValue,\n  useAnimationFrame,\n  useInView,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type WebhookRelayHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<WebhookRelayHue, string> = {\n  sky: \"[--hue-hi:var(--color-sky-400)] [--hue:var(--color-sky-500)] [--hue-deep:var(--color-sky-700)]\",\n  emerald: \"[--hue-hi:var(--color-emerald-400)] [--hue:var(--color-emerald-500)] [--hue-deep:var(--color-emerald-700)]\",\n  amber: \"[--hue-hi:var(--color-amber-400)] [--hue:var(--color-amber-500)] [--hue-deep:var(--color-amber-700)]\",\n  violet: \"[--hue-hi:var(--color-violet-400)] [--hue:var(--color-violet-500)] [--hue-deep:var(--color-violet-700)]\",\n  rose: \"[--hue-hi:var(--color-rose-400)] [--hue:var(--color-rose-500)] [--hue-deep:var(--color-rose-700)]\",\n  orange: \"[--hue-hi:var(--color-orange-400)] [--hue:var(--color-orange-500)] [--hue-deep:var(--color-orange-700)]\",\n  indigo: \"[--hue-hi:var(--color-indigo-400)] [--hue:var(--color-indigo-500)] [--hue-deep:var(--color-indigo-700)]\",\n  teal: \"[--hue-hi:var(--color-teal-400)] [--hue:var(--color-teal-500)] [--hue-deep:var(--color-teal-700)]\",\n};\n\nexport interface WebhookEndpoint {\n  name: string;\n  path: string;\n  icon?: LucideIcon;\n}\n\nexport interface WebhookRelayProps {\n  source?: string;\n  event?: string;\n  endpoints?: [WebhookEndpoint, WebhookEndpoint, WebhookEndpoint];\n  failing?: 0 | 1 | 2;\n  title?: string;\n  description?: string;\n  hue?: WebhookRelayHue;\n  className?: string;\n}\n\ntype Pt = [number, number];\ntype State = \"idle\" | \"pending\" | \"ok\" | \"fail\";\n\nconst W = 400;\nconst H = 200;\nconst MID = 100;\nconst SRC: Pt = [44, MID];\nconst RELAY: Pt = [150, MID];\nconst ENDS: Pt[] = [\n  [326, 34],\n  [326, MID],\n  [326, 166],\n];\nconst NODE = 40;\nconst HUB = 50;\nconst LOOP = 10.4;\nconst RESET = 9.7;\nconst SETTLED = 8.6;\n\nconst SEND = [1.2, 1.26, 1.32];\nconst TRAVEL = 0.8;\nconst BOUNCE = 0.42;\nconst BACKOFF = [1, 2];\n\nconst defaults: [WebhookEndpoint, WebhookEndpoint, WebhookEndpoint] = [\n  { name: \"Fulfilment\", path: \"POST /hooks/orders\", icon: TruckIcon },\n  { name: \"Ledger\", path: \"POST /v2/events\", icon: WalletIcon },\n  { name: \"Email worker\", path: \"POST /notify\", icon: MailIcon },\n];\n\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\nconst seg = (t: number, a: number, b: number) => clamp01((t - a) / (b - a));\nconst easeInOut = (v: number) => (v < 0.5 ? 4 * v * v * v : 1 - (-2 * v + 2) ** 3 / 2);\nconst easeOut = (v: number) => 1 - (1 - v) ** 3;\n\nfunction wire(i: number): [Pt, Pt, Pt, Pt] {\n  const end = ENDS[i];\n  const from: Pt = [RELAY[0] + HUB / 2, MID];\n  const to: Pt = [end[0] - NODE / 2, end[1]];\n  return [from, [from[0] + 70, MID], [to[0] - 70, end[1]], to];\n}\n\nfunction bezier([a, b, c, d]: [Pt, Pt, Pt, Pt], t: number): Pt {\n  const u = 1 - t;\n  return [\n    u * u * u * a[0] + 3 * u * u * t * b[0] + 3 * u * t * t * c[0] + t * t * t * d[0],\n    u * u * u * a[1] + 3 * u * u * t * b[1] + 3 * u * t * t * c[1] + t * t * t * d[1],\n  ];\n}\n\nfunction retryTimeline() {\n  const arrive1 = SEND[1] + TRAVEL;\n  const wait1 = arrive1 + BOUNCE;\n  const send2 = wait1 + BACKOFF[0];\n  const arrive2 = send2 + TRAVEL;\n  const wait2 = arrive2 + BOUNCE;\n  const send3 = wait2 + BACKOFF[1];\n  const arrive3 = send3 + TRAVEL;\n  return { arrive1, wait1, send2, arrive2, wait2, send3, arrive3 };\n}\n\nconst T = retryTimeline();\n\nfunction failingProgress(t: number, start: number) {\n  const shift = start - SEND[1];\n  const s = t - shift;\n  if (s < SEND[1]) return { p: 0, show: 0 };\n  if (s < T.arrive1) return { p: easeInOut(seg(s, SEND[1], T.arrive1)), show: 1 };\n  if (s < T.wait1) return { p: 1 - easeOut(seg(s, T.arrive1, T.wait1)), show: 1 - seg(s, T.wait1 - 0.08, T.wait1) };\n  if (s < T.send2) return { p: 0, show: 0 };\n  if (s < T.arrive2) return { p: easeInOut(seg(s, T.send2, T.arrive2)), show: 1 };\n  if (s < T.wait2) return { p: 1 - easeOut(seg(s, T.arrive2, T.wait2)), show: 1 - seg(s, T.wait2 - 0.08, T.wait2) };\n  if (s < T.send3) return { p: 0, show: 0 };\n  if (s < T.arrive3) return { p: easeInOut(seg(s, T.send3, T.arrive3)), show: 1 };\n  return { p: 1, show: 1 - seg(s, T.arrive3, T.arrive3 + 0.18) };\n}\n\nfunction snapshot(t: number, failing: number) {\n  if (t >= RESET || t < 0.15) return { signed: false, ends: [\"idle\", \"idle\", \"idle\"] as State[], retry: 0, delivered: 0 };\n  const ends = [0, 1, 2].map((i): State => {\n    const arrive = SEND[i] + TRAVEL;\n    if (i !== failing) return t >= arrive ? \"ok\" : t >= SEND[i] ? \"pending\" : \"idle\";\n    const s = t - (SEND[i] - SEND[1]);\n    if (s >= T.arrive3) return \"ok\";\n    if (s >= T.send3) return \"pending\";\n    if (s >= T.arrive2) return \"fail\";\n    if (s >= T.send2) return \"pending\";\n    if (s >= T.arrive1) return \"fail\";\n    if (s >= SEND[1]) return \"pending\";\n    return \"idle\";\n  });\n  const s = t - (SEND[failing] - SEND[1]);\n  const retry = s >= T.wait1 && s < T.send2 ? 1 : s >= T.wait2 && s < T.send3 ? 2 : 0;\n  return { signed: t >= 0.95, ends, retry, delivered: ends.filter((e) => e === \"ok\").length };\n}\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction Packet({ x, y, opacity, signed }: { x: MotionValue<number>; y: MotionValue<number>; opacity: MotionValue<number>; signed: boolean }) {\n  return (\n    <motion.g style={{ opacity, x, y }}>\n      <rect className={signed ? \"fill-(--hue)\" : \"fill-foreground/45\"} height={11} rx={3} width={15} x={-7.5} y={-5.5} />\n      <path className=\"fill-none stroke-white/80\" d=\"M-5 -3 L0 0.8 L5 -3\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.4} />\n      {signed && <circle className=\"fill-amber-300 stroke-card\" cx={6.5} cy={4.5} r={2.6} strokeWidth={1} />}\n    </motion.g>\n  );\n}\n\nfunction useTravel(clock: MotionValue<number>, path: (t: number) => { p: number; show: number }, curve: [Pt, Pt, Pt, Pt]) {\n  const x = useTransform(clock, (t) => bezier(curve, path(t).p)[0]);\n  const y = useTransform(clock, (t) => bezier(curve, path(t).p)[1]);\n  const opacity = useTransform(clock, (t) => path(t).show);\n  return { x, y, opacity };\n}\n\nexport function WebhookRelay({\n  source = \"orders-api\",\n  event = \"order.paid\",\n  endpoints = defaults,\n  failing = 1,\n  title = \"Every event, delivered\",\n  description = \"Signed at the relay, retried with backoff, and logged until each endpoint answers 200.\",\n  hue = \"indigo\",\n  className,\n}: WebhookRelayProps) {\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const [hovered, setHovered] = useState<number | null>(null);\n  const running = inView && hovered === null && !reduce;\n  const live = useRef(running);\n  live.current = running;\n\n  const clock = useMotionValue(0);\n  useAnimationFrame((_, delta) => {\n    if (!live.current) return;\n    clock.set((clock.get() + Math.min(delta, 64) / 1000) % LOOP);\n  });\n\n  useEffect(() => {\n    if (reduce) clock.set(SETTLED);\n  }, [reduce, clock]);\n\n  const [snap, setSnap] = useState(() => snapshot(0, failing));\n  const key = useRef(\"\");\n  useMotionValueEvent(clock, \"change\", (t) => {\n    const next = snapshot(t, failing);\n    const k = JSON.stringify(next);\n    if (k === key.current) return;\n    key.current = k;\n    setSnap(next);\n  });\n\n  const inbound = useTravel(\n    clock,\n    (t) => ({ p: easeInOut(seg(t, 0.2, 0.9)), show: t < 0.2 ? 0 : 1 - seg(t, 0.86, 0.96) }),\n    [[SRC[0] + NODE / 2, MID], [SRC[0] + 50, MID], [RELAY[0] - 50, MID], [RELAY[0] - HUB / 2, MID]],\n  );\n  const fan = [0, 1, 2].map((i) =>\n    i === failing\n      ? (t: number) => failingProgress(t, SEND[i])\n      : (t: number) => {\n          const a = SEND[i];\n          return { p: easeInOut(seg(t, a, a + TRAVEL)), show: t < a ? 0 : 1 - seg(t, a + TRAVEL, a + TRAVEL + 0.18) };\n        },\n  );\n  const p0 = useTravel(clock, fan[0], wire(0));\n  const p1 = useTravel(clock, fan[1], wire(1));\n  const p2 = useTravel(clock, fan[2], wire(2));\n  const outbound = [p0, p1, p2];\n\n  const s = (t: number) => t - (SEND[failing] - SEND[1]);\n  const ring = useTransform(clock, (t) => {\n    const v = s(t);\n    if (v >= T.wait1 && v < T.send2) return seg(v, T.wait1, T.send2);\n    if (v >= T.wait2 && v < T.send3) return seg(v, T.wait2, T.send3);\n    return 0;\n  });\n\n  const hubPulse = useMotionValue(1);\n  useEffect(() => {\n    if (!snap.signed || reduce) return;\n    const c = animate(hubPulse, [1, 1.08, 1], { duration: 0.36, ease: [0.23, 1, 0.32, 1] });\n    return () => c.stop();\n  }, [snap.signed, reduce, hubPulse]);\n\n  const retryWire = wire(failing);\n  const retryAt = bezier(retryWire, 0.66);\n\n  const labels = [\n    { name: source, detail: `emits ${event}`, at: SRC, size: NODE, align: \"start\" as const },\n    { name: \"Relay\", detail: `HMAC-signed · backoff ${BACKOFF.join(\"s, \")}s`, at: RELAY, size: HUB, align: \"center\" as const },\n    ...endpoints.map((e, i) => ({\n      name: e.name,\n      detail: `${e.path} · ${snap.ends[i] === \"ok\" ? \"200\" : snap.ends[i] === \"fail\" ? \"500\" : snap.ends[i] === \"pending\" ? \"sending\" : \"waiting\"}`,\n      at: ENDS[i],\n      size: NODE,\n      align: \"end\" as const,\n    })),\n  ];\n\n  return (\n    <div\n      ref={root}\n      className={cn(\"relative w-full max-w-[460px] overflow-hidden rounded-2xl border bg-card p-4 shadow-xs/5 sm:p-6\", hues[hue], className)}\n    >\n      <div\n        aria-hidden\n        className=\"dot-grid pointer-events-none absolute inset-0 opacity-50 [mask-image:radial-gradient(closest-side,black,transparent)]\"\n      />\n\n      <div className=\"relative mx-auto mb-3 flex w-fit items-center gap-1.5 rounded-full border bg-popover py-1 pr-2.5 pl-2 text-xs shadow-xs/5\">\n        <WebhookIcon aria-hidden className=\"size-3.5 text-muted-foreground\" />\n        <span className=\"font-medium font-mono text-foreground\">{event}</span>\n        <span className=\"text-muted-foreground tabular-nums\">· {snap.delivered}/3 delivered</span>\n      </div>\n\n      <div className=\"relative mx-auto aspect-[400/200] w-full max-w-[400px]\">\n        <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`0 0 ${W} ${H}`}>\n          <g className=\"fill-none stroke-foreground/15\" strokeLinecap=\"round\" strokeWidth={2}>\n            <path d={`M${SRC[0] + NODE / 2} ${MID} H${RELAY[0] - HUB / 2}`} />\n            {[0, 1, 2].map((i) => {\n              const [a, b, c, d] = wire(i);\n              return (\n                <path\n                  className={cn(\n                    \"transition-[stroke] duration-300\",\n                    snap.ends[i] === \"fail\" && \"stroke-red-500/45\",\n                    snap.ends[i] === \"ok\" && \"stroke-emerald-500/45\",\n                  )}\n                  d={`M${a[0]} ${a[1]} C${b[0]} ${b[1]} ${c[0]} ${c[1]} ${d[0]} ${d[1]}`}\n                  key={i}\n                />\n              );\n            })}\n          </g>\n\n          <Packet {...inbound} signed={false} />\n          {outbound.map((p, i) => (\n            <Packet key={i} {...p} signed />\n          ))}\n\n          <g transform={`translate(${SRC[0]} ${SRC[1]})`}>\n            <rect className=\"fill-card stroke-foreground/15\" height={NODE} rx={12} width={NODE} x={-NODE / 2} y={-NODE / 2} />\n            <ServerIcon className=\"text-foreground/70\" height={18} width={18} x={-9} y={-9} />\n          </g>\n\n          <motion.g style={{ originX: `${RELAY[0]}px`, originY: `${MID}px`, scale: hubPulse, transformBox: \"view-box\" }}>\n            <rect className=\"fill-(--hue)\" height={HUB} rx={15} width={HUB} x={RELAY[0] - HUB / 2} y={MID - HUB / 2} />\n            <rect className=\"fill-white/20\" height={3} rx={1.5} width={HUB - 24} x={RELAY[0] - HUB / 2 + 12} y={MID - HUB / 2 + 5} />\n            <WebhookIcon className=\"text-white\" height={22} width={22} x={RELAY[0] - 11} y={MID - 11} />\n            <motion.g\n              animate={{ opacity: snap.signed ? 1 : 0, scale: snap.signed || reduce ? 1 : 0.6 }}\n              initial={false}\n              style={{ originX: `${RELAY[0] + 19}px`, originY: `${MID - 19}px`, transformBox: \"view-box\" }}\n              transition={reduce ? { duration: 0.15 } : snap.signed ? { type: \"spring\", stiffness: 520, damping: 24 } : { duration: 0.2 }}\n            >\n              <circle className=\"fill-amber-300 stroke-card\" cx={RELAY[0] + 19} cy={MID - 19} r={7} strokeWidth={2} />\n              <path\n                className=\"fill-none stroke-amber-900\"\n                d={`M${RELAY[0] + 16} ${MID - 19} l2 2 l3.5 -3.8`}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth={1.6}\n              />\n            </motion.g>\n          </motion.g>\n\n          {ENDS.map((at, i) => {\n            const state = snap.ends[i];\n            const Icon = endpoints[i].icon ?? ServerIcon;\n            return (\n              <g key={i}>\n                <motion.g\n                  animate={state === \"fail\" && !reduce ? { x: [0, -3, 3, -2, 0] } : { x: 0 }}\n                  key={state === \"fail\" ? `shake-${i}-${snap.retry}` : `still-${i}`}\n                  transition={{ duration: 0.32, ease: \"easeOut\" }}\n                >\n                  <rect\n                    className={cn(\n                      \"fill-card stroke-foreground/15 transition-[stroke,fill] duration-300\",\n                      state === \"ok\" && \"fill-emerald-500/10 stroke-emerald-500\",\n                      state === \"fail\" && \"fill-red-500/10 stroke-red-500\",\n                    )}\n                    height={NODE}\n                    rx={12}\n                    strokeWidth={state === \"ok\" || state === \"fail\" ? 1.5 : 1}\n                    width={NODE}\n                    x={at[0] - NODE / 2}\n                    y={at[1] - NODE / 2}\n                  />\n                  <Icon className=\"text-foreground/70\" height={18} width={18} x={at[0] - 9} y={at[1] - 9} />\n                </motion.g>\n                <text\n                  className={cn(\n                    \"font-medium font-mono tabular-nums transition-[fill,opacity] duration-300\",\n                    state === \"ok\" ? \"fill-emerald-600 dark:fill-emerald-400\" : \"fill-red-600 dark:fill-red-400\",\n                    state === \"ok\" || state === \"fail\" ? \"opacity-100\" : \"opacity-0\",\n                  )}\n                  dominantBaseline=\"central\"\n                  fontSize={11}\n                  x={at[0] + NODE / 2 + 6}\n                  y={at[1]}\n                >\n                  {state === \"ok\" ? \"200\" : \"500\"}\n                </text>\n              </g>\n            );\n          })}\n\n          <AnimatePresence>\n            {snap.retry > 0 && (\n              <motion.g\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: reduce ? 1 : 0.95 }}\n                initial={{ opacity: 0, scale: reduce ? 1 : 0.95 }}\n                key={snap.retry}\n                style={{ originX: `${retryAt[0]}px`, originY: `${retryAt[1]}px`, transformBox: \"view-box\" }}\n                transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}\n              >\n                <rect className=\"fill-popover stroke-foreground/15\" height={22} rx={11} width={80} x={retryAt[0] - 40} y={retryAt[1] - 11} />\n                <circle className=\"fill-none stroke-foreground/15\" cx={retryAt[0] - 28} cy={retryAt[1]} r={5} strokeWidth={2} />\n                <motion.circle\n                  className=\"fill-none stroke-amber-500\"\n                  cx={retryAt[0] - 28}\n                  cy={retryAt[1]}\n                  r={5}\n                  strokeLinecap=\"round\"\n                  strokeWidth={2}\n                  style={{ pathLength: ring, rotate: -90, originX: \"50%\", originY: \"50%\" }}\n                />\n                <text className=\"fill-foreground font-medium tabular-nums\" dominantBaseline=\"central\" fontSize={10.5} x={retryAt[0] - 19} y={retryAt[1] + 0.5}>\n                  retry in {BACKOFF[snap.retry - 1]}s\n                </text>\n              </motion.g>\n            )}\n          </AnimatePresence>\n        </svg>\n\n        {labels.map((l, i) => {\n          const open = hovered === i;\n          const below = i === 2;\n          return (\n            <button\n              aria-label={`${l.name}: ${l.detail}`}\n              className=\"absolute -translate-x-1/2 -translate-y-1/2 cursor-default rounded-[30%] outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n              key={l.name}\n              onBlur={() => setHovered(null)}\n              onFocus={() => setHovered(i)}\n              onPointerEnter={(e) => e.pointerType === \"mouse\" && setHovered(i)}\n              onPointerLeave={() => setHovered((h) => (h === i ? null : h))}\n              style={{ height: `${(l.size / H) * 100}%`, left: `${(l.at[0] / W) * 100}%`, top: `${(l.at[1] / H) * 100}%`, width: `${(l.size / W) * 100}%` }}\n              type=\"button\"\n            >\n              <AnimatePresence>\n                {open && (\n                  <motion.span\n                    animate={{ opacity: 1, scale: 1 }}\n                    className={cn(\n                      \"pointer-events-none absolute z-10 flex flex-col items-start whitespace-nowrap rounded-lg border bg-popover px-2 py-1 text-left shadow-xs/5\",\n                      below ? \"top-full mt-1.5\" : \"bottom-full mb-1.5\",\n                      l.align === \"start\" && \"left-0 origin-bottom-left\",\n                      l.align === \"center\" && \"left-1/2 -translate-x-1/2 origin-bottom\",\n                      l.align === \"end\" && (below ? \"right-0 origin-top-right\" : \"right-0 origin-bottom-right\"),\n                    )}\n                    exit={{ opacity: 0, scale: 0.97 }}\n                    initial={{ opacity: 0, scale: 0.95 }}\n                    transition={{ duration: 0.15, ease: [0.23, 1, 0.32, 1] }}\n                  >\n                    <span className=\"font-medium text-foreground text-xs\">{l.name}</span>\n                    <span className=\"font-mono text-[11px] text-muted-foreground\">{l.detail}</span>\n                  </motion.span>\n                )}\n              </AnimatePresence>\n            </button>\n          );\n        })}\n      </div>\n\n      <div className=\"relative mt-3 text-center\">\n        <p className=\"text-balance font-medium text-foreground text-sm\">{title}</p>\n        <p className=\"mx-auto mt-1 max-w-[36ch] text-pretty text-muted-foreground text-xs\">{description}</p>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}