{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "organization-chart",
  "title": "Organization Chart",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/organization-chart/OrganizationChart.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, ref, watch } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { organizationChartVariants } from './organization-chart.variants'\nimport OrgChartNode from './OrgChartNode.vue'\nimport type { OrgNode } from './types'\n\ninterface Props {\n  data: OrgNode\n  direction?: 'top-down' | 'left-right'\n  defaultExpanded?: boolean\n  showConnectors?: boolean\n  zoomable?: boolean\n  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  direction: 'top-down',\n  defaultExpanded: true,\n  showConnectors: true,\n  zoomable: false,\n})\n\nconst emit = defineEmits<{\n  nodeClick: [node: OrgNode]\n  toggle: [node: OrgNode, expanded: boolean]\n}>()\n\nconst expanded = ref<Set<string>>(new Set())\n\nfunction collectIds(node: OrgNode, acc: string[] = []): string[] {\n  acc.push(node.id)\n  if (node.children) for (const c of node.children) collectIds(c, acc)\n  return acc\n}\n\nfunction defaultExpand() {\n  if (props.defaultExpanded) {\n    expanded.value = new Set(collectIds(props.data))\n  } else {\n    expanded.value = new Set([props.data.id])\n  }\n}\n\nwatch(\n  () => [props.data, props.defaultExpanded],\n  () => defaultExpand(),\n  { immediate: true },\n)\n\nfunction toggleNode(node: OrgNode) {\n  const next = new Set(expanded.value)\n  if (next.has(node.id)) next.delete(node.id)\n  else next.add(node.id)\n  expanded.value = next\n  emit('toggle', node, next.has(node.id))\n}\n\nfunction isExpanded(node: OrgNode): boolean {\n  return expanded.value.has(node.id)\n}\n\nfunction expandAll() {\n  expanded.value = new Set(collectIds(props.data))\n}\n\nfunction collapseAll() {\n  expanded.value = new Set([props.data.id])\n}\n\nconst zoom = ref(1)\nfunction zoomIn() {\n  zoom.value = Math.min(2, zoom.value + 0.1)\n}\nfunction zoomOut() {\n  zoom.value = Math.max(0.5, zoom.value - 0.1)\n}\nfunction resetZoom() {\n  zoom.value = 1\n}\n\nconst containerStyle = computed(() => ({\n  transform: `scale(${zoom.value})`,\n  transformOrigin: 'top center',\n}))\n\ndefineExpose({ expandAll, collapseAll, zoomIn, zoomOut, resetZoom })\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"organization-chart\"\n    :data-direction=\"direction\"\n    :class=\"cn(organizationChartVariants(), props.class)\"\n  >\n    <div v-if=\"zoomable\" class=\"border-border flex items-center gap-2 border-b px-3 py-2\">\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-7 items-center justify-center rounded-md text-sm\"\n        aria-label=\"Zoom out\"\n        @click=\"zoomOut\"\n      >\n        −\n      </button>\n      <span class=\"text-muted-foreground w-12 text-center text-xs tabular-nums\">{{ Math.round(zoom * 100) }}%</span>\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-7 items-center justify-center rounded-md text-sm\"\n        aria-label=\"Zoom in\"\n        @click=\"zoomIn\"\n      >\n        +\n      </button>\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground hover:bg-accent ml-1 rounded-md px-2 py-1 text-xs\"\n        aria-label=\"Reset zoom\"\n        @click=\"resetZoom\"\n      >\n        Reset\n      </button>\n      <div class=\"ml-auto flex gap-1\">\n        <button\n          type=\"button\"\n          class=\"text-muted-foreground hover:text-foreground hover:bg-accent rounded-md px-2 py-1 text-xs\"\n          @click=\"expandAll\"\n        >\n          Expand all\n        </button>\n        <button\n          type=\"button\"\n          class=\"text-muted-foreground hover:text-foreground hover:bg-accent rounded-md px-2 py-1 text-xs\"\n          @click=\"collapseAll\"\n        >\n          Collapse all\n        </button>\n      </div>\n    </div>\n    <div class=\"overflow-auto p-4\">\n      <div :style=\"containerStyle\" class=\"transition-transform duration-200\">\n        <OrgChartNode\n          :node=\"data\"\n          :depth=\"0\"\n          :is-root=\"true\"\n          :direction=\"direction\"\n          :show-connectors=\"showConnectors\"\n          :is-expanded=\"isExpanded\"\n          :toggle=\"toggleNode\"\n          @node-click=\"(n) => emit('nodeClick', n)\"\n        >\n          <template #node=\"{ node }\">\n            <slot name=\"node\" :node=\"node\" />\n          </template>\n        </OrgChartNode>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/organization-chart/OrganizationChart.vue"
    },
    {
      "path": "packages/registry-vue/components/organization-chart/OrgChartNode.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent } from 'vue'\nimport { ChevronDown, ChevronRight } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport type { OrgNode } from './types'\n\n// Self-reference for recursive rendering\nconst OrgChartNode = defineAsyncComponent(() => import('./OrgChartNode.vue'))\n\ninterface Props {\n  node: OrgNode\n  depth: number\n  isRoot?: boolean\n  direction?: 'top-down' | 'left-right'\n  showConnectors?: boolean\n  isExpanded: (node: OrgNode) => boolean\n  toggle: (node: OrgNode) => void\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  isRoot: false,\n  direction: 'top-down',\n  showConnectors: true,\n})\n\nconst emit = defineEmits<{\n  nodeClick: [node: OrgNode]\n}>()\n\nconst open = computed(() => props.isExpanded(props.node))\nconst hasChildren = computed(() => !!props.node.children?.length)\nconst isHorizontal = computed(() => props.direction === 'left-right')\nconst childCount = computed(() => props.node.children?.length ?? 0)\nconst isOnlyChild = computed(() => childCount.value <= 1)\n\nfunction onClick() {\n  emit('nodeClick', props.node)\n}\n\nfunction onToggle(e: Event) {\n  e.stopPropagation()\n  if (hasChildren.value) props.toggle(props.node)\n}\n\nfunction initials(name: string): string {\n  return name.split(' ').map((p) => p[0]).slice(0, 2).join('').toUpperCase()\n}\n</script>\n\n<template>\n  <!-- ══ Top-down (vertical) layout ══ -->\n  <div v-if=\"!isHorizontal\" class=\"org-v\" :data-root=\"isRoot ? '' : undefined\">\n    <!-- Node card -->\n    <div class=\"org-v-card\">\n      <div\n        class=\"bg-card hover:bg-accent/50 border-border group relative flex w-52 cursor-pointer flex-col rounded-lg border p-3 shadow-xs transition-colors\"\n        :class=\"isRoot ? 'ring-primary/20 ring-2' : ''\"\n        @click=\"onClick\"\n      >\n        <div class=\"flex items-center gap-2.5\">\n          <img\n            v-if=\"node.avatar\"\n            :src=\"node.avatar\"\n            :alt=\"node.name\"\n            class=\"border-border size-10 shrink-0 rounded-full border object-cover\"\n          />\n          <div\n            v-else\n            class=\"bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-semibold\"\n          >\n            {{ initials(node.name) }}\n          </div>\n          <div class=\"min-w-0 flex-1\">\n            <p class=\"truncate text-sm font-semibold\">{{ node.name }}</p>\n            <p v-if=\"node.title\" class=\"text-muted-foreground truncate text-xs\">{{ node.title }}</p>\n          </div>\n          <button\n            v-if=\"hasChildren\"\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-5 shrink-0 items-center justify-center rounded\"\n            :aria-expanded=\"open\"\n            :aria-label=\"open ? 'Collapse' : 'Expand'\"\n            @click=\"onToggle\"\n          >\n            <ChevronDown v-if=\"open\" class=\"size-3.5\" />\n            <ChevronRight v-else class=\"size-3.5\" />\n          </button>\n        </div>\n        <slot name=\"node\" :node=\"node\" />\n      </div>\n    </div>\n\n    <!-- Children -->\n    <div v-if=\"hasChildren && open\" class=\"org-v-children\">\n      <!-- Vertical line from parent to horizontal sibling bar -->\n      <div v-if=\"showConnectors\" class=\"org-v-line-down\" />\n      <div class=\"org-v-children-row\" :data-single=\"isOnlyChild ? '' : undefined\">\n        <!-- Horizontal bar connecting siblings (only for 2+ children) -->\n        <div v-if=\"showConnectors && !isOnlyChild\" class=\"org-v-line-across\" />\n        <OrgChartNode\n          v-for=\"child in node.children\"\n          :key=\"child.id\"\n          :node=\"child\"\n          :depth=\"depth + 1\"\n          :is-root=\"false\"\n          :direction=\"direction\"\n          :show-connectors=\"showConnectors\"\n          :is-expanded=\"isExpanded\"\n          :toggle=\"toggle\"\n          @node-click=\"(n) => emit('nodeClick', n)\"\n        >\n          <template #node=\"{ node: n }\">\n            <slot name=\"node\" :node=\"n\" />\n          </template>\n        </OrgChartNode>\n      </div>\n    </div>\n  </div>\n\n  <!-- ══ Left-right (horizontal) layout ══ -->\n  <div v-else class=\"org-h\" :data-root=\"isRoot ? '' : undefined\">\n    <div class=\"flex items-start\">\n      <!-- Node card -->\n      <div class=\"org-h-card\">\n        <div\n          class=\"bg-card hover:bg-accent/50 border-border group relative flex w-52 cursor-pointer flex-col rounded-lg border p-3 shadow-xs transition-colors\"\n          :class=\"isRoot ? 'ring-primary/20 ring-2' : ''\"\n          @click=\"onClick\"\n        >\n          <div class=\"flex items-center gap-2.5\">\n            <img\n              v-if=\"node.avatar\"\n              :src=\"node.avatar\"\n              :alt=\"node.name\"\n              class=\"border-border size-10 shrink-0 rounded-full border object-cover\"\n            />\n            <div\n              v-else\n              class=\"bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-semibold\"\n            >\n              {{ initials(node.name) }}\n            </div>\n            <div class=\"min-w-0 flex-1\">\n              <p class=\"truncate text-sm font-semibold\">{{ node.name }}</p>\n              <p v-if=\"node.title\" class=\"text-muted-foreground truncate text-xs\">{{ node.title }}</p>\n            </div>\n            <button\n              v-if=\"hasChildren\"\n              type=\"button\"\n              class=\"text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-5 shrink-0 items-center justify-center rounded\"\n              @click=\"onToggle\"\n            >\n              <ChevronDown v-if=\"open\" class=\"size-3.5\" />\n              <ChevronRight v-else class=\"size-3.5\" />\n            </button>\n          </div>\n          <slot name=\"node\" :node=\"node\" />\n        </div>\n      </div>\n\n      <!-- Children -->\n      <template v-if=\"hasChildren && open\">\n        <div v-if=\"showConnectors\" class=\"org-h-line-right\" />\n        <div class=\"org-h-children\">\n          <OrgChartNode\n            v-for=\"child in node.children\"\n            :key=\"child.id\"\n            :node=\"child\"\n            :depth=\"depth + 1\"\n            :is-root=\"false\"\n            :direction=\"direction\"\n            :show-connectors=\"showConnectors\"\n            :is-expanded=\"isExpanded\"\n            :toggle=\"toggle\"\n            @node-click=\"(n) => emit('nodeClick', n)\"\n          >\n            <template #node=\"{ node: n }\">\n              <slot name=\"node\" :node=\"n\" />\n            </template>\n          </OrgChartNode>\n        </div>\n      </template>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n/* ══ Vertical (top-down) layout ══ */\n.org-v {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n/* Vertical line from horizontal bar up to each child card */\n.org-v:not([data-root]) .org-v-card {\n  position: relative;\n  padding-top: 20px;\n}\n.org-v:not([data-root]) .org-v-card::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: 50%;\n  width: 1px;\n  height: 20px;\n  background: var(--color-border, hsl(var(--border)));\n}\n\n.org-v-children {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n/* Vertical line from parent down to the sibling bar */\n.org-v-line-down {\n  width: 1px;\n  height: 20px;\n  background: var(--color-border, hsl(var(--border)));\n}\n\n.org-v-children-row {\n  display: flex;\n  flex-direction: row;\n  gap: 24px;\n  position: relative;\n  padding-top: 20px;\n}\n\n/* Horizontal bar: spans from the center of the first child to the center\n   of the last child. We use a full-width bar with the first/last child\n   vertical lines connecting to it. The bar itself is positioned using\n   the half-width of the first and last cards (w-52 = 208px, half = 104px). */\n.org-v-line-across {\n  position: absolute;\n  top: 0;\n  left: 104px; /* half of w-52 (208px) — center of first child */\n  right: 104px; /* half of w-52 — center of last child */\n  height: 1px;\n  background: var(--color-border, hsl(var(--border)));\n}\n\n/* When single child, no horizontal bar needed — just the vertical line */\n.org-v-children-row[data-single] .org-v-line-across {\n  display: none;\n}\n\n/* ══ Horizontal (left-right) layout ══ */\n.org-h {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n/* Horizontal line from parent to children column */\n.org-h-line-right {\n  width: 20px;\n  height: 1px;\n  background: var(--color-border, hsl(var(--border)));\n  margin-top: 40px;\n  flex-shrink: 0;\n}\n\n.org-h-children {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  position: relative;\n}\n\n/* Vertical line connecting siblings in horizontal mode */\n.org-h-children::before {\n  content: '';\n  position: absolute;\n  left: 0;\n  top: 40px;\n  bottom: 40px;\n  width: 1px;\n  background: var(--color-border, hsl(var(--border)));\n}\n\n/* Horizontal line from vertical bar to each child */\n.org-h:not([data-root]) .org-h-card {\n  position: relative;\n  padding-left: 20px;\n}\n.org-h:not([data-root]) .org-h-card::before {\n  content: '';\n  position: absolute;\n  top: 40px;\n  left: 0;\n  width: 20px;\n  height: 1px;\n  background: var(--color-border, hsl(var(--border)));\n}\n</style>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/organization-chart/OrgChartNode.vue"
    },
    {
      "path": "packages/registry-vue/components/organization-chart/organization-chart.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\nexport const organizationChartVariants = cva('bg-background rounded-lg border')\n\nexport type OrganizationChartVariants = VariantProps<typeof organizationChartVariants>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/organization-chart/organization-chart.variants.ts"
    },
    {
      "path": "packages/registry-vue/components/organization-chart/types.ts",
      "content": "export interface OrgNode {\n  id: string\n  name: string\n  title?: string\n  avatar?: string\n  department?: string\n  children?: OrgNode[]\n  [key: string]: unknown\n}\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/organization-chart/types.ts"
    },
    {
      "path": "packages/registry-vue/components/organization-chart/index.ts",
      "content": "export { default as OrganizationChart } from './OrganizationChart.vue'\nexport { default as OrgChartNode } from './OrgChartNode.vue'\nexport { organizationChartVariants, type OrganizationChartVariants } from './organization-chart.variants'\nexport type { OrgNode } from './types'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/organization-chart/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Organization hierarchy visualization with a tree of nodes (name, title, avatar). Expand/collapse branches, top-down or left-right direction, connector lines, optional zoom/pan controls, node click events, and a customizable node slot.",
  "categories": [
    "display",
    "data"
  ]
}