{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "webhooks",
  "title": "Webhooks",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/webhooks/webhooks.component.ts",
      "content": "// Webhook manager in two SectionCards: endpoints with event chips, live success rate\n// and a pause/resume switch, plus a delivery log pairing relative timestamps with HTTP\n// status pills, durations, retry counts and retry actions. Includes an add-endpoint dialog.\n// Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport { LucideAngularModule, Plus, RotateCw, Webhook } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiChipComponent } from '@/ui/chip/chip.component'\nimport {\n  UiDialogComponent,\n  UiDialogContentComponent,\n  UiDialogDescriptionComponent,\n  UiDialogFooterComponent,\n  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport { UiRelativeTimeComponent } from '@/ui/relative-time/relative-time.component'\nimport { UiSectionCardComponent } from '@/ui/section-card/section-card.component'\n\nexport type WebhookEvent = 'order.created' | 'order.refunded' | 'customer.updated' | 'invoice.paid'\n\nexport interface WebhookEndpoint {\n  id: string\n  url: string\n  enabled: boolean\n  events: WebhookEvent[]\n  successRate: string\n}\n\nexport interface WebhookDelivery {\n  id: string\n  endpointId: string\n  event: WebhookEvent\n  status: number\n  durationMs: number\n  retries: number\n  date: Date\n}\n\nconst now = Date.now()\nconst minutesAgo = (m: number) => new Date(now - m * 60_000)\n\nconst stubEndpoints: WebhookEndpoint[] = [\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: WebhookDelivery[] = [\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 cloneEndpoints(list: WebhookEndpoint[]): WebhookEndpoint[] {\n  return list.map((e) => ({ ...e, events: [...e.events] }))\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-webhooks, [ui-webhooks]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiChipComponent,\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogFooterComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiInputComponent,\n    UiRelativeTimeComponent,\n    UiSectionCardComponent,\n  ],\n  template: `\n    <div data-slot=\"webhooks\" [class]=\"rootClass\">\n      <div ui-section-card title=\"Endpoints\" description=\"HTTP endpoints that receive signed webhook payloads.\">\n        <button ui-button size=\"sm\" slot=\"header-action\" (click)=\"createOpen.set(true)\">\n          <lucide-icon [img]=\"Plus\" aria-hidden=\"true\" />\n          Add endpoint\n        </button>\n\n        <ul class=\"-mb-4 divide-y\">\n          @for (endpoint of endpointList(); track endpoint.id) {\n            <li 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                <lucide-icon [img]=\"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                  @for (event of endpoint.events; track event) {\n                    <span ui-chip variant=\"outline\" size=\"sm\">{{ event }}</span>\n                  }\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                [attr.aria-checked]=\"endpoint.enabled\"\n                [attr.aria-label]=\"endpoint.enabled ? 'Pause endpoint' : 'Resume endpoint'\"\n                [class]=\"switchClass(endpoint.enabled)\"\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                ></span>\n              </button>\n            </li>\n          }\n        </ul>\n      </div>\n\n      <div ui-section-card title=\"Recent deliveries\" description=\"Latest payload attempts across all endpoints.\">\n        <ul class=\"-my-4 divide-y\">\n          @for (delivery of deliveryList(); track delivery.id) {\n            <li class=\"flex flex-wrap items-center gap-3 py-3\">\n              <ui-relative-time [date]=\"delivery.date\" class=\"text-muted-foreground w-16 shrink-0 text-xs\" />\n              <span ui-badge [variant]=\"statusTone(delivery.status)\" class=\"w-12 shrink-0 justify-center font-mono\">\n                {{ delivery.status }}\n              </span>\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                {{ formatDuration(delivery.durationMs) }}\n              </p>\n              @if (delivery.retries > 0) {\n                <p class=\"text-warning shrink-0 text-xs font-medium\">retry {{ delivery.retries }}</p>\n              }\n              @if (delivery.status >= 400) {\n                <button ui-button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Retry delivery\">\n                  <lucide-icon [img]=\"RotateCw\" aria-hidden=\"true\" />\n                </button>\n              }\n            </li>\n          }\n          @if (deliveryList().length === 0) {\n            <li class=\"text-muted-foreground py-8 text-center text-xs\">\n              No deliveries yet — payloads will appear here once endpoints receive events.\n            </li>\n          }\n        </ul>\n      </div>\n\n      <ui-dialog [open]=\"createOpen()\" (openChange)=\"createOpen.set($event)\">\n        <ui-dialog-content>\n          <div ui-dialog-header>\n            <h2 ui-dialog-title>Add endpoint</h2>\n            <p ui-dialog-description>Choose the events this URL should receive.</p>\n          </div>\n          <ui-input\n            [value]=\"newUrl()\"\n            (valueChange)=\"newUrl.set($event)\"\n            placeholder=\"https://api.yourapp.com/hooks/…\"\n            class=\"font-mono text-xs\"\n          />\n          <div class=\"flex flex-wrap gap-2\">\n            @for (event of allEvents; track event) {\n              <button\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                <span ui-chip [variant]=\"newEvents().includes(event) ? 'filled' : 'outline'\">{{ event }}</span>\n              </button>\n            }\n          </div>\n          <div ui-dialog-footer>\n            <button ui-button variant=\"outline\" (click)=\"createOpen.set(false)\">Cancel</button>\n            <button ui-button [disabled]=\"!newUrl() || newEvents().length === 0\" (click)=\"createEndpoint()\">\n              Create\n            </button>\n          </div>\n        </ui-dialog-content>\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiWebhooksComponent {\n  protected readonly Plus = Plus\n  protected readonly RotateCw = RotateCw\n  protected readonly Webhook = Webhook\n\n  protected readonly allEvents = allEvents\n\n  @Input() set endpoints(v: WebhookEndpoint[] | undefined) {\n    this.endpointList.set(cloneEndpoints(v ?? stubEndpoints))\n  }\n  @Input() set deliveries(v: WebhookDelivery[] | undefined) {\n    this.deliveryList.set(v ?? stubDeliveries)\n  }\n  @Input('class') className?: string\n\n  readonly endpointList = signal<WebhookEndpoint[]>(cloneEndpoints(stubEndpoints))\n  readonly deliveryList = signal<WebhookDelivery[]>(stubDeliveries)\n\n  readonly createOpen = signal(false)\n  readonly newUrl = signal('')\n  readonly newEvents = signal<WebhookEvent[]>([])\n\n  readonly endpointCount = computed(() => this.endpointList().length)\n\n  get rootClass(): string {\n    return cn('space-y-4', this.className)\n  }\n\n  switchClass(enabled: boolean): string {\n    return cn(\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      enabled ? 'bg-primary' : 'bg-muted-foreground/30',\n    )\n  }\n\n  toggleEndpoint(endpoint: WebhookEndpoint): void {\n    this.endpointList.update((list) => list.map((e) => (e.id === endpoint.id ? { ...e, enabled: !e.enabled } : e)))\n  }\n\n  toggleEvent(event: WebhookEvent): void {\n    this.newEvents.update((events) => (events.includes(event) ? events.filter((e) => e !== event) : [...events, event]))\n  }\n\n  createEndpoint(): void {\n    if (!this.newUrl() || this.newEvents().length === 0) return\n    this.endpointList.update((list) => [\n      ...list,\n      { id: `e${list.length + 1}`, url: this.newUrl(), enabled: true, events: [...this.newEvents()], successRate: '—' },\n    ])\n    this.newUrl.set('')\n    this.newEvents.set([])\n    this.createOpen.set(false)\n  }\n\n  statusTone(status: number): 'success' | 'warning' | 'destructive' {\n    if (status < 300) return 'success'\n    if (status < 500) return 'warning'\n    return 'destructive'\n  }\n\n  formatDuration(durationMs: number): string {\n    return durationMs >= 1000 ? `${(durationMs / 1000).toFixed(1)} s` : `${durationMs} ms`\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/webhooks/webhooks.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/webhooks/index.ts",
      "content": "export {\n  UiWebhooksComponent,\n  type WebhookEvent,\n  type WebhookEndpoint,\n  type WebhookDelivery,\n} from './webhooks.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/webhooks/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/chip.json",
    "https://uipkge.dev/r/angular/dialog.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/relative-time.json",
    "https://uipkge.dev/r/angular/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"
  ]
}