{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "integrations-hub-diagram",
  "title": "Integrations — Hub Diagram",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/integrations-hub-diagram/IntegrationsHubDiagram.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { ArrowRight, Boxes } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\n\nconst nodes = [\n  { mark: 'SF', name: 'Snowflake', direction: 'source' },\n  { mark: 'BQ', name: 'BigQuery', direction: 'source' },\n  { mark: 'PG', name: 'Postgres', direction: 'source' },\n  { mark: 'DT', name: 'dbt', direction: 'source' },\n  { mark: 'SL', name: 'Slack', direction: 'destination' },\n  { mark: 'PD', name: 'PagerDuty', direction: 'destination' },\n  { mark: 'LK', name: 'Looker', direction: 'destination' },\n  { mark: 'HX', name: 'Hex', direction: 'destination' },\n]\n\n// Positions are computed on a circle in a 100x100 user-space viewBox, so any\n// number of nodes distributes evenly without touching the markup.\nconst RADIUS = 38\nconst placed = computed(() =>\n  nodes.map((node, index) => {\n    const angle = (index / nodes.length) * Math.PI * 2 - Math.PI / 2\n    return {\n      ...node,\n      x: 50 + RADIUS * Math.cos(angle),\n      y: 50 + RADIUS * Math.sin(angle),\n    }\n  }),\n)\n</script>\n\n<template>\n  <section data-slot=\"integrations-hub-diagram\" class=\"bg-background\">\n    <div class=\"mx-auto max-w-6xl px-6 py-20 lg:py-28\">\n      <div class=\"grid items-center gap-12 lg:grid-cols-[1fr_1.1fr] lg:gap-16\">\n        <div>\n          <Badge variant=\"secondary\">Architecture</Badge>\n          <h2 class=\"mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl\">\n            One hub between your warehouse and everything downstream\n          </h2>\n          <p class=\"text-muted-foreground mt-3 leading-relaxed\">\n            Sources stay read-only and in place. Destinations receive certified metrics rather than raw extracts, so\n            nothing downstream can quietly redefine a number.\n          </p>\n\n          <dl class=\"mt-8 flex flex-wrap gap-x-8 gap-y-3 text-sm\">\n            <div class=\"flex items-center gap-2\">\n              <span class=\"bg-primary size-2 rounded-full\" aria-hidden=\"true\" />\n              <dt class=\"font-medium\">Sources</dt>\n              <dd class=\"text-muted-foreground\">read-only</dd>\n            </div>\n            <div class=\"flex items-center gap-2\">\n              <span class=\"bg-muted-foreground/50 size-2 rounded-full\" aria-hidden=\"true\" />\n              <dt class=\"font-medium\">Destinations</dt>\n              <dd class=\"text-muted-foreground\">certified metrics only</dd>\n            </div>\n          </dl>\n\n          <Button class=\"mt-8\">\n            See the reference architecture\n            <ArrowRight class=\"ml-2 size-4\" aria-hidden=\"true\" />\n          </Button>\n        </div>\n\n        <Card>\n          <CardContent class=\"p-6\">\n            <div class=\"relative aspect-square\">\n              <!-- Spokes are drawn in the same 100-unit space the nodes are\n                   placed in, so line ends always meet the node centres. -->\n              <svg class=\"absolute inset-0 size-full\" viewBox=\"0 0 100 100\" aria-hidden=\"true\">\n                <line\n                  v-for=\"node in placed\"\n                  :key=\"node.name\"\n                  x1=\"50\"\n                  y1=\"50\"\n                  :x2=\"node.x\"\n                  :y2=\"node.y\"\n                  class=\"stroke-border\"\n                  stroke-width=\"0.4\"\n                  :stroke-dasharray=\"node.direction === 'destination' ? '2 2' : undefined\"\n                />\n                <circle cx=\"50\" cy=\"50\" :r=\"RADIUS\" class=\"stroke-border/60\" fill=\"none\" stroke-width=\"0.3\" />\n              </svg>\n\n              <div\n                class=\"border-border bg-card absolute top-1/2 left-1/2 flex size-20 -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center gap-1 rounded-xl border shadow-sm\"\n              >\n                <Boxes class=\"text-primary size-5\" aria-hidden=\"true\" />\n                <span class=\"text-xs font-semibold tracking-tight\">Metric layer</span>\n              </div>\n\n              <div\n                v-for=\"node in placed\"\n                :key=\"node.name\"\n                class=\"absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-1\"\n                :style=\"{ left: `${node.x}%`, top: `${node.y}%` }\"\n              >\n                <span\n                  class=\"border-border bg-background flex size-10 items-center justify-center rounded-lg border font-mono text-xs font-semibold\"\n                  :class=\"node.direction === 'source' ? 'text-foreground' : 'text-muted-foreground'\"\n                  aria-hidden=\"true\"\n                >\n                  {{ node.mark }}\n                </span>\n                <span class=\"text-muted-foreground text-xs whitespace-nowrap\">{{ node.name }}</span>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n    </div>\n  </section>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/IntegrationsHubDiagram.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/card.json"
  ],
  "description": "Radial integration diagram: a central platform node connected by drawn SVG spokes to eight orbiting service nodes, with a legend splitting sources from destinations.",
  "categories": [
    "marketing"
  ]
}