{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "webhooks",
  "title": "Webhooks",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/webhooks/Webhooks.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Plus, RotateCw, Webhook } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Chip } from '@/components/ui/chip'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '@/components/ui/dialog'\nimport { Input } from '@/components/ui/input'\nimport { RelativeTime } from '@/components/ui/relative-time'\nimport { SectionCard } from '@/components/ui/section-card'\n\ntype WebhookEvent = 'order.created' | 'order.refunded' | 'customer.updated' | 'invoice.paid'\n\nexport interface Endpoint {\n  id: string\n  url: string\n  enabled: boolean\n  events: WebhookEvent[]\n  successRate: string\n}\n\nexport interface Delivery {\n  id: string\n  endpointId: string\n  event: WebhookEvent\n  status: number\n  durationMs: number\n  retries: number\n  date: Date\n}\n\nexport interface WebhooksProps {\n  endpoints?: Endpoint[]\n  deliveries?: Delivery[]\n  className?: string\n}\n\nconst now = Date.now()\nconst minutesAgo = (m: number) => new Date(now - m * 60_000)\n\nconst stubEndpoints: Endpoint[] = [\n  {\n    id: 'e1',\n    url: 'https://api.acme.com/hooks/orders',\n    enabled: true,\n    events: ['order.created', 'order.refunded'],\n    successRate: '99.2% · 24h',\n  },\n  {\n    id: 'e2',\n    url: 'https://hooks.billing.io/acme',\n    enabled: true,\n    events: ['invoice.paid'],\n    successRate: '97.8% · 24h',\n  },\n  {\n    id: 'e3',\n    url: 'https://staging.acme.dev/hooks/all',\n    enabled: false,\n    events: ['order.created', 'customer.updated', 'invoice.paid'],\n    successRate: '—',\n  },\n]\n\nconst stubDeliveries: Delivery[] = [\n  { id: 'd1', endpointId: 'e1', event: 'order.created', status: 200, durationMs: 312, retries: 0, date: minutesAgo(2) },\n  { id: 'd2', endpointId: 'e2', event: 'invoice.paid', status: 200, durationMs: 188, retries: 0, date: minutesAgo(9) },\n  {\n    id: 'd3',\n    endpointId: 'e1',\n    event: 'order.refunded',\n    status: 500,\n    durationMs: 10_240,\n    retries: 2,\n    date: minutesAgo(26),\n  },\n  { id: 'd4', endpointId: 'e1', event: 'order.created', status: 404, durationMs: 96, retries: 1, date: minutesAgo(41) },\n  { id: 'd5', endpointId: 'e2', event: 'invoice.paid', status: 200, durationMs: 244, retries: 0, date: minutesAgo(58) },\n  {\n    id: 'd6',\n    endpointId: 'e3',\n    event: 'customer.updated',\n    status: 410,\n    durationMs: 51,\n    retries: 0,\n    date: minutesAgo(75),\n  },\n]\n\nconst allEvents: WebhookEvent[] = ['order.created', 'order.refunded', 'customer.updated', 'invoice.paid']\n\nfunction statusTone(status: number): 'success' | 'warning' | 'destructive' {\n  if (status < 300) return 'success'\n  if (status < 500) return 'warning'\n  return 'destructive'\n}\n\nexport function Webhooks({ endpoints, deliveries, className }: WebhooksProps) {\n  const [endpointList, setEndpointList] = React.useState<Endpoint[]>(endpoints ?? stubEndpoints)\n  const deliveryList = deliveries ?? stubDeliveries\n\n  const [createOpen, setCreateOpen] = React.useState(false)\n  const [newUrl, setNewUrl] = React.useState('')\n  const [newEvents, setNewEvents] = React.useState<WebhookEvent[]>([])\n\n  function toggleEndpoint(endpoint: Endpoint) {\n    setEndpointList((list) => list.map((e) => (e.id === endpoint.id ? { ...e, enabled: !e.enabled } : e)))\n  }\n\n  function toggleEvent(event: WebhookEvent) {\n    setNewEvents((events) => (events.includes(event) ? events.filter((e) => e !== event) : [...events, event]))\n  }\n\n  function createEndpoint() {\n    if (!newUrl || newEvents.length === 0) return\n    setEndpointList((list) => [\n      ...list,\n      { id: `e${list.length + 1}`, url: newUrl, enabled: true, events: [...newEvents], successRate: '—' },\n    ])\n    setNewUrl('')\n    setNewEvents([])\n    setCreateOpen(false)\n  }\n\n  return (\n    <div data-slot=\"webhooks\" className={cn('space-y-4', className)}>\n      <SectionCard\n        title=\"Endpoints\"\n        description=\"HTTP endpoints that receive signed webhook payloads.\"\n        headerAction={\n          <Dialog open={createOpen} onOpenChange={setCreateOpen}>\n            <DialogTrigger asChild>\n              <Button size=\"sm\">\n                <Plus aria-hidden=\"true\" />\n                Add endpoint\n              </Button>\n            </DialogTrigger>\n            <DialogContent>\n              <DialogHeader>\n                <DialogTitle>Add endpoint</DialogTitle>\n                <DialogDescription>Choose the events this URL should receive.</DialogDescription>\n              </DialogHeader>\n              <Input\n                value={newUrl}\n                onChange={(e) => setNewUrl(e.target.value)}\n                placeholder=\"https://api.yourapp.com/hooks/…\"\n                className=\"font-mono text-xs\"\n              />\n              <div className=\"flex flex-wrap gap-2\">\n                {allEvents.map((event) => (\n                  <button\n                    key={event}\n                    type=\"button\"\n                    className=\"focus-visible:ring-ring rounded-full focus-visible:ring-2 focus-visible:outline-none\"\n                    onClick={() => toggleEvent(event)}\n                  >\n                    <Chip variant={newEvents.includes(event) ? 'filled' : 'outline'}>{event}</Chip>\n                  </button>\n                ))}\n              </div>\n              <DialogFooter>\n                <Button variant=\"outline\" onClick={() => setCreateOpen(false)}>\n                  Cancel\n                </Button>\n                <Button disabled={!newUrl || newEvents.length === 0} onClick={createEndpoint}>\n                  Create\n                </Button>\n              </DialogFooter>\n            </DialogContent>\n          </Dialog>\n        }\n      >\n        <ul className=\"-mb-4 divide-y\">\n          {endpointList.map((endpoint) => (\n            <li key={endpoint.id} className=\"flex items-center gap-3 py-4\">\n              <span\n                className=\"bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-md\"\n                aria-hidden=\"true\"\n              >\n                <Webhook className=\"size-4\" />\n              </span>\n              <div className=\"min-w-0 flex-1\">\n                <p className=\"truncate font-mono text-xs font-medium\">{endpoint.url}</p>\n                <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n                  {endpoint.events.map((event) => (\n                    <Chip key={event} variant=\"outline\" size=\"sm\">\n                      {event}\n                    </Chip>\n                  ))}\n                </div>\n              </div>\n              <div className=\"hidden text-right sm:block\">\n                <p className=\"text-xs font-medium\">{endpoint.enabled ? endpoint.successRate : 'Paused'}</p>\n                <p className=\"text-muted-foreground text-xs\">{endpoint.events.length} events</p>\n              </div>\n              <button\n                type=\"button\"\n                role=\"switch\"\n                aria-checked={endpoint.enabled}\n                aria-label={endpoint.enabled ? 'Pause endpoint' : 'Resume endpoint'}\n                className={[\n                  'focus-visible:ring-ring relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full transition-colors focus:outline-none focus-visible:ring-2',\n                  endpoint.enabled ? 'bg-primary' : 'bg-muted-foreground/30',\n                ].join(' ')}\n                onClick={() => toggleEndpoint(endpoint)}\n              >\n                <span\n                  className={[\n                    'bg-background inline-block size-4 translate-y-0.5 rounded-full shadow transition-transform',\n                    endpoint.enabled ? 'translate-x-[18px]' : 'translate-x-0.5',\n                  ].join(' ')}\n                />\n              </button>\n            </li>\n          ))}\n        </ul>\n      </SectionCard>\n\n      <SectionCard title=\"Recent deliveries\" description=\"Latest payload attempts across all endpoints.\">\n        <ul className=\"-my-4 divide-y\">\n          {deliveryList.map((delivery) => (\n            <li key={delivery.id} className=\"flex flex-wrap items-center gap-3 py-3\">\n              <RelativeTime date={delivery.date} className=\"text-muted-foreground w-16 shrink-0 text-xs\" />\n              <Badge variant={statusTone(delivery.status)} className=\"w-12 shrink-0 justify-center font-mono\">\n                {delivery.status}\n              </Badge>\n              <p className=\"min-w-[10rem] flex-1 truncate text-sm\">{delivery.event}</p>\n              <p className=\"text-muted-foreground hidden w-20 shrink-0 text-right font-mono text-xs sm:block\">\n                {delivery.durationMs >= 1000\n                  ? `${(delivery.durationMs / 1000).toFixed(1)} s`\n                  : `${delivery.durationMs} ms`}\n              </p>\n              {delivery.retries > 0 && (\n                <p className=\"text-warning shrink-0 text-xs font-medium\">retry {delivery.retries}</p>\n              )}\n              {delivery.status >= 400 && (\n                <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Retry delivery\">\n                  <RotateCw aria-hidden=\"true\" />\n                </Button>\n              )}\n            </li>\n          ))}\n        </ul>\n      </SectionCard>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/Webhooks.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/chip.json",
    "https://uipkge.dev/r/react/dialog.json",
    "https://uipkge.dev/r/react/input.json",
    "https://uipkge.dev/r/react/relative-time.json",
    "https://uipkge.dev/r/react/section-card.json"
  ],
  "description": "Webhook manager in two SectionCards: endpoints with event chips, live success rate and a pause/resume switch, plus a delivery log pairing relative timestamps with HTTP status pills, durations, retry counts and retry actions. Includes an add-endpoint dialog. Stateful demo — swap the stub data for your source.",
  "categories": [
    "devops",
    "dashboard",
    "communication"
  ]
}