{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "webhook-tester",
  "title": "Webhook Tester",
  "description": "HTTP request testing UI with URL, method, headers, body inputs and response viewer",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/devtools/webhook-tester/components/elements/webhook-tester.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Loader2, Plus, Send, Trash2 } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\ninterface WebhookResponse {\n  status: number;\n  statusText: string;\n  headers: Record<string, string>;\n  body: unknown;\n  timing: number;\n}\n\ninterface WebhookTesterProps {\n  defaultUrl?: string;\n  defaultMethod?: HttpMethod;\n  defaultHeaders?: Record<string, string>;\n  defaultBody?: string;\n  onSend?: (request: {\n    url: string;\n    method: HttpMethod;\n    headers: Record<string, string>;\n    body?: string;\n  }) => Promise<WebhookResponse>;\n  className?: string;\n}\n\nconst METHOD_COLORS: Record<HttpMethod, string> = {\n  GET: \"bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300\",\n  POST: \"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300\",\n  PUT: \"bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300\",\n  PATCH:\n    \"bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300\",\n  DELETE: \"bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300\",\n};\n\nexport function WebhookTester({\n  defaultUrl = \"\",\n  defaultMethod = \"POST\",\n  defaultHeaders = { \"Content-Type\": \"application/json\" },\n  defaultBody = \"{}\",\n  onSend,\n  className,\n}: WebhookTesterProps) {\n  const [url, setUrl] = React.useState(defaultUrl);\n  const [method, setMethod] = React.useState<HttpMethod>(defaultMethod);\n  const [headers, setHeaders] = React.useState<\n    { key: string; value: string }[]\n  >(Object.entries(defaultHeaders).map(([key, value]) => ({ key, value })));\n  const [body, setBody] = React.useState(defaultBody);\n  const [response, setResponse] = React.useState<WebhookResponse | null>(null);\n  const [error, setError] = React.useState<string | null>(null);\n  const [loading, setLoading] = React.useState(false);\n\n  const handleAddHeader = React.useCallback(() => {\n    setHeaders((prev) => [...prev, { key: \"\", value: \"\" }]);\n  }, []);\n\n  const handleRemoveHeader = React.useCallback((index: number) => {\n    setHeaders((prev) => prev.filter((_, i) => i !== index));\n  }, []);\n\n  const handleHeaderChange = React.useCallback(\n    (index: number, field: \"key\" | \"value\", value: string) => {\n      setHeaders((prev) =>\n        prev.map((h, i) => (i === index ? { ...h, [field]: value } : h)),\n      );\n    },\n    [],\n  );\n\n  const handleSend = React.useCallback(async () => {\n    if (!url) return;\n\n    setLoading(true);\n    setError(null);\n    setResponse(null);\n\n    const headersObj = headers.reduce(\n      (acc, { key, value }) => {\n        if (key) acc[key] = value;\n        return acc;\n      },\n      {} as Record<string, string>,\n    );\n\n    try {\n      if (onSend) {\n        const res = await onSend({\n          url,\n          method,\n          headers: headersObj,\n          body: method !== \"GET\" ? body : undefined,\n        });\n        setResponse(res);\n      } else {\n        const start = performance.now();\n        const res = await fetch(url, {\n          method,\n          headers: headersObj,\n          body: method !== \"GET\" ? body : undefined,\n        });\n        const timing = performance.now() - start;\n\n        const responseHeaders: Record<string, string> = {};\n        res.headers.forEach((value, key) => {\n          responseHeaders[key] = value;\n        });\n\n        let responseBody: unknown;\n        const contentType = res.headers.get(\"content-type\");\n        if (contentType?.includes(\"application/json\")) {\n          responseBody = await res.json();\n        } else {\n          responseBody = await res.text();\n        }\n\n        setResponse({\n          status: res.status,\n          statusText: res.statusText,\n          headers: responseHeaders,\n          body: responseBody,\n          timing: Math.round(timing),\n        });\n      }\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"Request failed\");\n    } finally {\n      setLoading(false);\n    }\n  }, [url, method, headers, body, onSend]);\n\n  const getStatusColor = React.useCallback((status: number) => {\n    if (status >= 200 && status < 300) return \"text-green-600\";\n    if (status >= 300 && status < 400) return \"text-yellow-600\";\n    if (status >= 400 && status < 500) return \"text-orange-600\";\n    return \"text-red-600\";\n  }, []);\n\n  return (\n    <div\n      data-slot=\"webhook-tester\"\n      aria-busy={loading}\n      aria-label=\"Webhook tester\"\n      className={cn(\n        \"border border-border rounded-lg overflow-hidden\",\n        className,\n      )}\n    >\n      <div className=\"p-4 space-y-4\">\n        <div className=\"flex gap-2\">\n          <select\n            value={method}\n            onChange={(e) => setMethod(e.target.value as HttpMethod)}\n            aria-label=\"HTTP method\"\n            className={cn(\n              \"px-3 py-2 rounded font-mono text-sm font-medium border-none\",\n              METHOD_COLORS[method],\n            )}\n          >\n            {([\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as HttpMethod[]).map(\n              (m) => (\n                <option key={m} value={m}>\n                  {m}\n                </option>\n              ),\n            )}\n          </select>\n          <input\n            type=\"url\"\n            value={url}\n            onChange={(e) => setUrl(e.target.value)}\n            placeholder=\"https://api.example.com/webhook\"\n            aria-label=\"Webhook URL\"\n            className=\"flex-1 px-3 py-2 border border-border rounded bg-background font-mono text-sm\"\n          />\n          <button\n            type=\"button\"\n            onClick={handleSend}\n            disabled={loading || !url}\n            aria-label={loading ? \"Sending request\" : \"Send request\"}\n            className=\"flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded font-medium text-sm hover:bg-primary/90 disabled:opacity-50\"\n          >\n            {loading ? (\n              <Loader2 className=\"w-4 h-4 animate-spin\" />\n            ) : (\n              <Send className=\"w-4 h-4\" />\n            )}\n            Send\n          </button>\n        </div>\n\n        <div className=\"space-y-2\">\n          <div className=\"flex items-center justify-between\">\n            <label className=\"text-sm font-medium\">Headers</label>\n            <button\n              type=\"button\"\n              onClick={handleAddHeader}\n              aria-label=\"Add header\"\n              className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground\"\n            >\n              <Plus className=\"w-3 h-3\" />\n              Add\n            </button>\n          </div>\n          <div className=\"space-y-1\">\n            {headers.map((header, index) => (\n              <div key={index} className=\"flex gap-2\">\n                <input\n                  type=\"text\"\n                  value={header.key}\n                  onChange={(e) =>\n                    handleHeaderChange(index, \"key\", e.target.value)\n                  }\n                  placeholder=\"Header name\"\n                  aria-label=\"Header name\"\n                  className=\"flex-1 px-2 py-1 border border-border rounded bg-background font-mono text-sm\"\n                />\n                <input\n                  type=\"text\"\n                  value={header.value}\n                  onChange={(e) =>\n                    handleHeaderChange(index, \"value\", e.target.value)\n                  }\n                  placeholder=\"Header value\"\n                  aria-label={`Value for ${header.key || \"header\"}`}\n                  className=\"flex-[2] px-2 py-1 border border-border rounded bg-background font-mono text-sm\"\n                />\n                <button\n                  type=\"button\"\n                  onClick={() => handleRemoveHeader(index)}\n                  aria-label={`Remove ${header.key || \"header\"}`}\n                  className=\"p-1 text-muted-foreground hover:text-red-500\"\n                >\n                  <Trash2 className=\"w-4 h-4\" />\n                </button>\n              </div>\n            ))}\n          </div>\n        </div>\n\n        {method !== \"GET\" && (\n          <div className=\"space-y-2\">\n            <label className=\"text-sm font-medium\">Body</label>\n            <textarea\n              value={body}\n              onChange={(e) => setBody(e.target.value)}\n              placeholder='{\"key\": \"value\"}'\n              aria-label=\"Request body\"\n              className=\"w-full h-32 px-3 py-2 border border-border rounded bg-background font-mono text-sm resize-none\"\n            />\n          </div>\n        )}\n      </div>\n\n      {(response || error) && (\n        <div className=\"border-t border-border\">\n          <div className=\"px-4 py-2 bg-muted/50 text-sm font-medium flex items-center justify-between\">\n            <span>Response</span>\n            {response && (\n              <div className=\"flex items-center gap-3 text-xs\">\n                <span\n                  className={cn(\n                    \"font-mono font-bold\",\n                    getStatusColor(response.status),\n                  )}\n                >\n                  {response.status} {response.statusText}\n                </span>\n                <span className=\"text-muted-foreground\">\n                  {response.timing}ms\n                </span>\n              </div>\n            )}\n          </div>\n          <div className=\"p-4 overflow-auto max-h-64\">\n            {error ? (\n              <div className=\"text-red-500 text-sm\">{error}</div>\n            ) : response ? (\n              <pre className=\"font-mono text-xs whitespace-pre-wrap\">\n                {typeof response.body === \"string\"\n                  ? response.body\n                  : JSON.stringify(response.body, null, 2)}\n              </pre>\n            ) : null}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport type { WebhookTesterProps, WebhookResponse, HttpMethod };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Complete request builder: method selector, URL input, headers editor, JSON body, response display.",
  "categories": [
    "devtools"
  ],
  "type": "registry:ui"
}