{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "webhook-inspector",
  "type": "registry:component",
  "title": "Webhook Inspector",
  "description": "Incoming webhook deliveries with status and latency at a glance, a detail pane for payload, headers and your endpoint’s response, and Redeliver that slides a new attempt into the list and lets you watch it land.",
  "categories": [
    "component",
    "Dev Tools"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/webhook-inspector.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, CopyIcon, RotateCwIcon, WebhookIcon } from \"lucide-react\";\nimport { AnimatePresence, LayoutGroup, motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, type ReactNode, useEffect, useId, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type DeliveryStatus = number | \"pending\" | \"timeout\";\n\nexport interface WebhookDelivery {\n  id: string;\n  event: string;\n  status: DeliveryStatus;\n  latencyMs?: number;\n  sentAt: string;\n  attempt: number;\n  request: { headers: Record<string, string>; body: unknown };\n  response?: { headers?: Record<string, string>; body?: string };\n  redeliveryOf?: string;\n}\n\nexport interface WebhookInspectorProps {\n  endpoint: string;\n  deliveries: WebhookDelivery[];\n  onRedeliver?: (delivery: WebhookDelivery) => void;\n  className?: string;\n}\n\nconst ok = (s: DeliveryStatus) => typeof s === \"number\" && s >= 200 && s < 300;\n\nfunction statusStyle(s: DeliveryStatus) {\n  if (s === \"pending\") return \"bg-muted text-muted-foreground\";\n  if (ok(s)) return \"bg-success/10 text-success-foreground\";\n  if (typeof s === \"number\" && s >= 400 && s < 500) return \"bg-warning/12 text-warning-foreground\";\n  return \"bg-destructive/10 text-destructive-foreground\";\n}\n\nfunction statusLabel(s: DeliveryStatus) {\n  return s === \"pending\" ? \"…\" : s === \"timeout\" ? \"T/O\" : String(s);\n}\n\nfunction ago(iso: string, now: number) {\n  const s = Math.max(0, Math.round((now - new Date(iso).getTime()) / 1000));\n  if (s < 5) return \"just now\";\n  if (s < 60) return `${s}s ago`;\n  if (s < 3600) return `${Math.floor(s / 60)}m ago`;\n  if (s < 86400) return `${Math.floor(s / 3600)}h ago`;\n  return `${Math.floor(s / 86400)}d ago`;\n}\n\nfunction Json({ value }: { value: unknown }) {\n  const text = typeof value === \"string\" ? value : JSON.stringify(value, null, 2);\n  const re = /(\"(?:\\\\.|[^\"\\\\])*\")(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?/gi;\n  const out: ReactNode[] = [];\n  let last = 0;\n  for (const m of text.matchAll(re)) {\n    const at = m.index ?? 0;\n    if (at > last) out.push(<span className=\"text-muted-foreground\" key={`g${at}`}>{text.slice(last, at)}</span>);\n    if (m[1]) {\n      out.push(\n        <span className={m[2] ? \"text-sky-800 dark:text-sky-300\" : \"text-emerald-700 dark:text-emerald-400\"} key={`s${at}`}>\n          {m[1]}\n        </span>,\n      );\n      if (m[2]) out.push(<span className=\"text-muted-foreground\" key={`c${at}`}>{m[2]}</span>);\n    } else {\n      out.push(\n        <span className={m[3] ? \"text-rose-700 dark:text-rose-400\" : \"text-amber-700 dark:text-amber-300\"} key={`n${at}`}>\n          {m[0]}\n        </span>,\n      );\n    }\n    last = at + m[0].length;\n  }\n  if (last < text.length) out.push(<span className=\"text-muted-foreground\" key=\"end\">{text.slice(last)}</span>);\n  return <>{out}</>;\n}\n\nfunction Headers({ headers }: { headers: Record<string, string> }) {\n  return (\n    <dl className=\"grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1\">\n      {Object.entries(headers).map(([k, v]) => (\n        <div className=\"contents\" key={k}>\n          <dt className=\"text-sky-800 dark:text-sky-300\">{k}</dt>\n          <dd className=\"break-all\">{v}</dd>\n        </div>\n      ))}\n    </dl>\n  );\n}\n\ntype Tab = \"payload\" | \"headers\" | \"response\";\n\nexport function WebhookInspector({ endpoint, deliveries, onRedeliver, className }: WebhookInspectorProps) {\n  const reduce = useReducedMotion();\n  const uid = useId();\n  const [selectedId, setSelectedId] = useState(deliveries[0]?.id);\n  const [tab, setTab] = useState<Tab>(\"payload\");\n  const [now, setNow] = useState(() => Date.now());\n  const [copied, setCopied] = useState(false);\n  const known = useRef(new Set(deliveries.map((d) => d.id)));\n  const itemRefs = useRef(new Map<string, HTMLButtonElement>());\n\n  useEffect(() => {\n    const t = setInterval(() => setNow(Date.now()), 5000);\n    return () => clearInterval(t);\n  }, []);\n\n  useEffect(() => {\n    if (!copied) return;\n    const t = setTimeout(() => setCopied(false), 1400);\n    return () => clearTimeout(t);\n  }, [copied]);\n\n  useEffect(() => {\n    for (const d of deliveries) {\n      if (!known.current.has(d.id)) {\n        known.current.add(d.id);\n        if (d.redeliveryOf) setSelectedId(d.id);\n      }\n    }\n    setNow(Date.now());\n  }, [deliveries]);\n\n  const selected = deliveries.find((d) => d.id === selectedId) ?? deliveries[0];\n  const total = deliveries.filter((d) => d.status !== \"pending\");\n  const rate = total.length ? Math.round((total.filter((d) => ok(d.status)).length / total.length) * 100) : 100;\n  const pendingRedelivery = selected && deliveries.some((d) => d.redeliveryOf === selected.id && d.status === \"pending\");\n\n  const onListKey = (e: KeyboardEvent) => {\n    const i = deliveries.findIndex((d) => d.id === selected?.id);\n    const next = e.key === \"ArrowDown\" ? i + 1 : e.key === \"ArrowUp\" ? i - 1 : -1;\n    if (next < 0 || next >= deliveries.length) return;\n    e.preventDefault();\n    setSelectedId(deliveries[next].id);\n    itemRefs.current.get(deliveries[next].id)?.focus();\n  };\n\n  const summary = (d: WebhookDelivery) => {\n    if (d.status === \"pending\") return \"Sending…\";\n    if (d.status === \"timeout\") return `Timed out after ${((d.latencyMs ?? 30000) / 1000).toFixed(0)}s`;\n    if (ok(d.status)) return `Delivered in ${d.latencyMs?.toLocaleString(\"en-US\")} ms`;\n    return `Endpoint returned ${d.status} after ${d.latencyMs?.toLocaleString(\"en-US\")} ms`;\n  };\n\n  const copyText = selected\n    ? tab === \"payload\"\n      ? JSON.stringify(selected.request.body, null, 2)\n      : tab === \"headers\"\n        ? Object.entries(selected.request.headers)\n            .map(([k, v]) => `${k}: ${v}`)\n            .join(\"\\n\")\n        : (selected.response?.body ?? \"\")\n    : \"\";\n\n  return (\n    <div className={cn(\"flex w-full min-w-0 max-w-[640px] flex-col overflow-hidden rounded-2xl border bg-card text-sm shadow-xs/5\", className)}>\n      <div className=\"flex items-center gap-3 border-b px-4 py-3\">\n        <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-muted\">\n          <WebhookIcon className=\"size-4\" />\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"font-medium\">Webhook deliveries</p>\n          <p className=\"truncate font-mono text-muted-foreground text-xs\">{endpoint}</p>\n        </div>\n        <div className=\"flex shrink-0 flex-col items-end gap-0.5 text-xs\">\n          <span className=\"flex items-center gap-1.5\">\n            <span className=\"relative flex size-2\">\n              <span className=\"absolute inset-0 animate-ping rounded-full bg-success/50 motion-reduce:hidden\" />\n              <span className=\"relative size-2 rounded-full bg-success\" />\n            </span>\n            Listening\n          </span>\n          <span className=\"text-muted-foreground tabular-nums\">{rate}% delivered</span>\n        </div>\n      </div>\n\n      <div className=\"grid sm:grid-cols-[236px_minmax(0,1fr)]\">\n        <LayoutGroup id={uid}>\n          <div\n            aria-label=\"Deliveries\"\n            className=\"h-[208px] overflow-y-auto border-b p-1.5 sm:h-[384px] sm:border-e sm:border-b-0 [scrollbar-width:thin]\"\n            onKeyDown={onListKey}\n            role=\"listbox\"\n          >\n            <AnimatePresence initial={false}>\n              {deliveries.map((d) => {\n                const isSel = d.id === selected?.id;\n                return (\n                  <motion.button\n                    animate={{ opacity: 1, y: 0 }}\n                    aria-selected={isSel}\n                    className={cn(\n                      \"relative flex w-full flex-col gap-1 rounded-lg px-2.5 py-2 text-start outline-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-ring\",\n                      !isSel && \"[@media(hover:hover)]:hover:bg-accent/50\",\n                    )}\n                    initial={reduce ? false : { opacity: 0, y: -8 }}\n                    key={d.id}\n                    layout={reduce ? false : \"position\"}\n                    onClick={() => setSelectedId(d.id)}\n                    ref={(el: HTMLButtonElement | null) => {\n                      if (el) itemRefs.current.set(d.id, el);\n                      else itemRefs.current.delete(d.id);\n                    }}\n                    role=\"option\"\n                    tabIndex={isSel ? 0 : -1}\n                    transition={{ type: \"spring\", stiffness: 500, damping: 40 }}\n                    type=\"button\"\n                  >\n                    {isSel && (\n                      <motion.span\n                        className=\"absolute inset-0 rounded-lg bg-accent\"\n                        layoutId={`${uid}-sel`}\n                        transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 550, damping: 42 }}\n                      />\n                    )}\n                    <span className=\"relative flex items-center gap-2\">\n                      <span\n                        className={cn(\n                          \"inline-flex h-5 w-10 shrink-0 items-center justify-center rounded-md font-medium font-mono text-[10.5px] tabular-nums transition-colors duration-200\",\n                          statusStyle(d.status),\n                        )}\n                      >\n                        {d.status === \"pending\" ? (\n                          <span className=\"size-2.5 animate-spin rounded-full border-[1.5px] border-current border-r-transparent\" />\n                        ) : (\n                          statusLabel(d.status)\n                        )}\n                      </span>\n                      <span className=\"min-w-0 flex-1 truncate font-mono text-xs\">{d.event}</span>\n                    </span>\n                    <span className=\"relative flex items-center gap-2 ps-12 text-[11px] text-muted-foreground\">\n                      <span className=\"min-w-0 flex-1 truncate\" suppressHydrationWarning>\n                        {ago(d.sentAt, now)}\n                        {d.attempt > 1 && ` · attempt ${d.attempt}`}\n                      </span>\n                      <span className=\"shrink-0 font-mono tabular-nums\">\n                        {d.status === \"pending\" ? \"—\" : d.status === \"timeout\" ? \"30s\" : `${d.latencyMs?.toLocaleString(\"en-US\")}ms`}\n                      </span>\n                    </span>\n                  </motion.button>\n                );\n              })}\n            </AnimatePresence>\n          </div>\n        </LayoutGroup>\n\n        {selected && (\n          <div className=\"flex min-w-0 flex-col sm:h-[384px]\">\n            <div className=\"flex items-start gap-3 px-4 pt-3.5 pb-3\">\n              <div className=\"min-w-0 flex-1\">\n                <p className=\"truncate font-medium font-mono text-sm\">{selected.event}</p>\n                <AnimatePresence initial={false} mode=\"popLayout\">\n                  <motion.p\n                    animate={{ opacity: 1 }}\n                    className={cn(\n                      \"mt-0.5 truncate text-xs\",\n                      selected.status === \"pending\" ? \"text-muted-foreground\" : ok(selected.status) ? \"text-success-foreground\" : \"text-destructive-foreground\",\n                    )}\n                    exit={{ opacity: 0 }}\n                    initial={{ opacity: 0 }}\n                    key={`${selected.id}-${selected.status}`}\n                    transition={{ duration: 0.15 }}\n                  >\n                    {summary(selected)}\n                  </motion.p>\n                </AnimatePresence>\n              </div>\n              <Button\n                className=\"shrink-0\"\n                disabled={selected.status === \"pending\" || pendingRedelivery}\n                onClick={() => onRedeliver?.(selected)}\n                size=\"sm\"\n                variant=\"outline\"\n              >\n                <RotateCwIcon className={cn(pendingRedelivery && \"animate-spin\")} />\n                Redeliver\n              </Button>\n            </div>\n\n            <div className=\"flex items-center gap-1 border-b px-3\">\n              {([\"payload\", \"headers\", \"response\"] as Tab[]).map((t) => (\n                <button\n                  aria-pressed={tab === t}\n                  className={cn(\n                    \"relative h-9 rounded-md px-2 text-xs capitalize outline-none transition-colors duration-150 focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n                    tab === t ? \"text-foreground\" : \"text-muted-foreground [@media(hover:hover)]:hover:text-foreground\",\n                  )}\n                  key={t}\n                  onClick={() => setTab(t)}\n                  type=\"button\"\n                >\n                  {t}\n                  {tab === t && (\n                    <motion.span\n                      className=\"absolute inset-x-1.5 -bottom-px h-0.5 rounded-full bg-primary\"\n                      layoutId={`${uid}-tab`}\n                      transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 600, damping: 45 }}\n                    />\n                  )}\n                </button>\n              ))}\n              <button\n                aria-label={copied ? \"Copied\" : `Copy ${tab}`}\n                className=\"ms-auto grid size-8 place-items-center rounded-lg text-muted-foreground outline-none transition-[color,background-color,scale] duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97] [@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-foreground\"\n                onClick={async () => {\n                  try {\n                    await navigator.clipboard.writeText(copyText);\n                  } catch {}\n                  setCopied(true);\n                }}\n                type=\"button\"\n              >\n                {copied ? <CheckIcon className=\"size-3.5 text-success-foreground\" /> : <CopyIcon className=\"size-3.5\" />}\n              </button>\n            </div>\n\n            <div className=\"min-h-[180px] flex-1 overflow-auto bg-code px-4 py-3 font-mono text-[11.5px] leading-5 [scrollbar-width:thin]\">\n              {tab === \"payload\" && (\n                <pre>\n                  <Json value={selected.request.body} />\n                </pre>\n              )}\n              {tab === \"headers\" && <Headers headers={selected.request.headers} />}\n              {tab === \"response\" &&\n                (selected.status === \"pending\" ? (\n                  <p className=\"font-sans text-muted-foreground text-xs\">Waiting for your endpoint to answer…</p>\n                ) : selected.status === \"timeout\" ? (\n                  <div className=\"font-sans text-xs\">\n                    <p className=\"font-medium text-destructive-foreground\">No response within 30 seconds</p>\n                    <p className=\"mt-1 text-muted-foreground\">Respond with a 2xx quickly and do the work in a background job.</p>\n                  </div>\n                ) : (\n                  <div className=\"flex flex-col gap-3\">\n                    <div className=\"flex items-center gap-2 font-sans text-xs\">\n                      <span className={cn(\"rounded-md px-1.5 font-medium font-mono leading-5\", statusStyle(selected.status))}>\n                        HTTP {selected.status}\n                      </span>\n                      <span className=\"text-muted-foreground tabular-nums\">{selected.latencyMs?.toLocaleString(\"en-US\")} ms</span>\n                    </div>\n                    {selected.response?.headers && <Headers headers={selected.response.headers} />}\n                    {selected.response?.body && (\n                      <pre className=\"whitespace-pre-wrap break-words border-t pt-3\">\n                        <Json value={selected.response.body} />\n                      </pre>\n                    )}\n                  </div>\n                ))}\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}