{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "webhooks",
  "title": "Webhooks",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/webhooks/Webhooks.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { Plus, RotateCw, Webhook } from 'lucide-vue-next'\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\ninterface Endpoint {\n  id: string\n  url: string\n  enabled: boolean\n  events: WebhookEvent[]\n  successRate: string\n}\n\ninterface Delivery {\n  id: string\n  endpointId: string\n  event: WebhookEvent\n  status: number\n  durationMs: number\n  retries: number\n  date: Date\n}\n\nconst props = defineProps<{\n  endpoints?: Endpoint[]\n  deliveries?: Delivery[]\n  class?: HTMLAttributes['class']\n}>()\n\nconst now = new Date()\nconst minutesAgo = (m: number) => new Date(now.getTime() - 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 endpoints = computed(() => props.endpoints ?? stubEndpoints)\nconst deliveries = computed(() => props.deliveries ?? stubDeliveries)\n\nconst allEvents: WebhookEvent[] = ['order.created', 'order.refunded', 'customer.updated', 'invoice.paid']\n\nconst createOpen = ref(false)\nconst newUrl = ref('')\nconst newEvents = ref<WebhookEvent[]>([])\n\nfunction toggleEndpoint(endpoint: Endpoint) {\n  endpoint.enabled = !endpoint.enabled\n}\n\nfunction toggleEvent(event: WebhookEvent) {\n  newEvents.value = newEvents.value.includes(event)\n    ? newEvents.value.filter((e) => e !== event)\n    : [...newEvents.value, event]\n}\n\nfunction createEndpoint() {\n  if (!newUrl.value || newEvents.value.length === 0) return\n  endpoints.value.push({\n    id: `e${endpoints.value.length + 1}`,\n    url: newUrl.value,\n    enabled: true,\n    events: [...newEvents.value],\n    successRate: '—',\n  })\n  newUrl.value = ''\n  newEvents.value = []\n  createOpen.value = false\n}\n\nfunction statusTone(status: number) {\n  if (status < 300) return 'success' as const\n  if (status < 500) return 'warning' as const\n  return 'destructive' as const\n}\n</script>\n\n<template>\n  <div data-slot=\"webhooks\" :class=\"cn('space-y-4', props.class)\">\n    <SectionCard title=\"Endpoints\" description=\"HTTP endpoints that receive signed webhook payloads.\">\n      <template #header-action>\n        <Dialog v-model:open=\"createOpen\">\n          <DialogTrigger as-child>\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 v-model=\"newUrl\" placeholder=\"https://api.yourapp.com/hooks/…\" class=\"font-mono text-xs\" />\n            <div class=\"flex flex-wrap gap-2\">\n              <button\n                v-for=\"event in allEvents\"\n                :key=\"event\"\n                type=\"button\"\n                class=\"focus-visible:ring-ring rounded-full focus-visible:ring-2 focus-visible:outline-none\"\n                @click=\"toggleEvent(event)\"\n              >\n                <Chip :variant=\"newEvents.includes(event) ? 'filled' : 'outline'\">{{ event }}</Chip>\n              </button>\n            </div>\n            <DialogFooter>\n              <Button variant=\"outline\" @click=\"createOpen = false\">Cancel</Button>\n              <Button :disabled=\"!newUrl || newEvents.length === 0\" @click=\"createEndpoint\">Create</Button>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n      </template>\n\n      <ul class=\"-mb-4 divide-y\">\n        <li v-for=\"endpoint in endpoints\" :key=\"endpoint.id\" class=\"flex items-center gap-3 py-4\">\n          <span\n            class=\"bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-md\"\n            aria-hidden=\"true\"\n          >\n            <Webhook class=\"size-4\" />\n          </span>\n          <div class=\"min-w-0 flex-1\">\n            <p class=\"truncate font-mono text-xs font-medium\">{{ endpoint.url }}</p>\n            <div class=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n              <Chip v-for=\"event in endpoint.events\" :key=\"event\" variant=\"outline\" size=\"sm\">{{ event }}</Chip>\n            </div>\n          </div>\n          <div class=\"hidden text-right sm:block\">\n            <p class=\"text-xs font-medium\">{{ endpoint.enabled ? endpoint.successRate : 'Paused' }}</p>\n            <p class=\"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            class=\"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            :class=\"endpoint.enabled ? 'bg-primary' : 'bg-muted-foreground/30'\"\n            @click=\"toggleEndpoint(endpoint)\"\n          >\n            <span\n              class=\"bg-background inline-block size-4 translate-y-0.5 rounded-full shadow transition-transform\"\n              :class=\"endpoint.enabled ? 'translate-x-[18px]' : 'translate-x-0.5'\"\n            />\n          </button>\n        </li>\n      </ul>\n    </SectionCard>\n\n    <SectionCard title=\"Recent deliveries\" description=\"Latest payload attempts across all endpoints.\">\n      <ul class=\"-my-4 divide-y\">\n        <li v-for=\"delivery in deliveries\" :key=\"delivery.id\" class=\"flex flex-wrap items-center gap-3 py-3\">\n          <RelativeTime :date=\"delivery.date\" class=\"text-muted-foreground w-16 shrink-0 text-xs\" />\n          <Badge :variant=\"statusTone(delivery.status)\" class=\"w-12 shrink-0 justify-center font-mono\">\n            {{ delivery.status }}\n          </Badge>\n          <p class=\"min-w-[10rem] flex-1 truncate text-sm\">{{ delivery.event }}</p>\n          <p class=\"text-muted-foreground hidden w-20 shrink-0 text-right font-mono text-xs sm:block\">\n            {{\n              delivery.durationMs >= 1000 ? `${(delivery.durationMs / 1000).toFixed(1)} s` : `${delivery.durationMs} ms`\n            }}\n          </p>\n          <p v-if=\"delivery.retries > 0\" class=\"text-warning shrink-0 text-xs font-medium\">\n            retry {{ delivery.retries }}\n          </p>\n          <Button v-if=\"delivery.status >= 400\" variant=\"ghost\" size=\"icon-sm\" aria-label=\"Retry delivery\">\n            <RotateCw aria-hidden=\"true\" />\n          </Button>\n        </li>\n      </ul>\n    </SectionCard>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/Webhooks.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/chip.json",
    "https://uipkge.dev/r/vue/dialog.json",
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/relative-time.json",
    "https://uipkge.dev/r/vue/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"
  ]
}