{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "organization-chart",
  "title": "Organization Chart",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/organization-chart/organization-chart.component.ts",
      "content": "import {\n  Component,\n  Directive,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  TemplateRef,\n  ViewContainerRef,\n  ViewEncapsulation,\n  booleanAttribute,\n  forwardRef,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiRenderTemplateDirective } from '@/ui/popper/popper'\nimport { organizationChartVariants, type OrganizationChartVariants } from './organization-chart.variants'\nimport type { OrgNode } from './types'\n\nexport type OrgChartDirection = 'top-down' | 'left-right'\nexport interface OrgNodeContext {\n  $implicit: OrgNode\n  node: OrgNode\n}\nexport interface OrgChartToggleEvent {\n  node: OrgNode\n  expanded: boolean\n}\n\n/* Connector lines, verbatim from React's organization-chart.css. */\nconst ORG_CHART_CSS = `\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`\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 initials(name: string): string {\n  return name\n    .split(' ')\n    .map((p) => p[0])\n    .slice(0, 2)\n    .join('')\n    .toUpperCase()\n}\n\n/** Renders the `renderNode` template with the node as context. */\n@Directive({ selector: '[uiOrgChartNodeOutlet]', standalone: true })\nexport class UiOrgChartNodeOutletDirective implements OnChanges, OnDestroy {\n  @Input('uiOrgChartNodeOutlet') template: TemplateRef<OrgNodeContext> | null | undefined = null\n  @Input('uiOrgChartNodeOutletNode') node!: OrgNode\n  private readonly vcr = inject(ViewContainerRef)\n\n  ngOnChanges(): void {\n    this.vcr.clear()\n    if (this.template) this.vcr.createEmbeddedView(this.template, { $implicit: this.node, node: this.node })\n  }\n\n  ngOnDestroy(): void {\n    this.vcr.clear()\n  }\n}\n\n/**\n * One node of the chart (React OrgChartNode): the card (role=button, Enter / Space\n * activate), avatar or initials, an expand toggle, and its children, recursively, with\n * the `org-v-*` / `org-h-*` connector classes. Put it on a <div> for React's DOM.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-org-chart-node, [ui-org-chart-node]',\n  standalone: true,\n  imports: [forwardRef(() => UiOrgChartNodeComponent), UiRenderTemplateDirective, UiOrgChartNodeOutletDirective],\n  host: {\n    '[class]': 'isHorizontal ? \"org-h\" : \"org-v\"',\n    '[attr.data-root]': 'isRoot ? \"\" : null',\n  },\n  template: `\n    <ng-template #card>\n      <div\n        role=\"button\"\n        tabindex=\"0\"\n        [attr.aria-label]=\"cardLabel\"\n        [class]=\"cardClass\"\n        (click)=\"onClick()\"\n        (keydown)=\"onCardKeyDown($event)\"\n      >\n        <div class=\"flex items-center gap-2.5\">\n          @if (current?.avatar) {\n            <img\n              [src]=\"node.avatar\"\n              [alt]=\"node.name\"\n              class=\"border-border size-10 shrink-0 rounded-full border object-cover\"\n            />\n          } @else {\n            <div\n              class=\"bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-semibold\"\n              aria-hidden=\"true\"\n            >\n              {{ initials }}\n            </div>\n          }\n          <div class=\"min-w-0 flex-1\">\n            <p class=\"truncate text-sm font-semibold\">{{ current?.name }}</p>\n            @if (current?.title) {\n              <p class=\"text-muted-foreground truncate text-xs\">{{ node.title }}</p>\n            }\n          </div>\n          @if (hasChildren) {\n            <button\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              [attr.aria-expanded]=\"open\"\n              [attr.aria-label]=\"open ? 'Collapse' : 'Expand'\"\n              (click)=\"onToggle($event)\"\n            >\n              @if (open) {\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"lucide lucide-chevron-down size-3.5\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"m6 9 6 6 6-6\" />\n                </svg>\n              } @else {\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"lucide lucide-chevron-right size-3.5\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"m9 18 6-6-6-6\" />\n                </svg>\n              }\n            </button>\n          }\n        </div>\n        <ng-container [uiOrgChartNodeOutlet]=\"renderNode\" [uiOrgChartNodeOutletNode]=\"node\" />\n      </div>\n    </ng-template>\n\n    @if (!isHorizontal) {\n      <div class=\"org-v-card\"><ng-container [uiRenderTemplate]=\"card\" /></div>\n      @if (hasChildren && open) {\n        <div class=\"org-v-children\">\n          @if (showConnectors) {\n            <div class=\"org-v-line-down\"></div>\n          }\n          <div class=\"org-v-children-row\" [attr.data-single]=\"isOnlyChild ? '' : null\">\n            @if (showConnectors && !isOnlyChild) {\n              <div class=\"org-v-line-across\"></div>\n            }\n            @for (child of current?.children; track child.id) {\n              <div\n                ui-org-chart-node\n                [node]=\"child\"\n                [depth]=\"depth + 1\"\n                [direction]=\"direction\"\n                [showConnectors]=\"showConnectors\"\n                [isExpanded]=\"isExpanded\"\n                [toggle]=\"toggle\"\n                [renderNode]=\"renderNode\"\n                (nodeClick)=\"nodeClick.emit($event)\"\n              ></div>\n            }\n          </div>\n        </div>\n      }\n    } @else {\n      <div class=\"flex items-start\">\n        <div class=\"org-h-card\"><ng-container [uiRenderTemplate]=\"card\" /></div>\n        @if (hasChildren && open) {\n          @if (showConnectors) {\n            <div class=\"org-h-line-right\"></div>\n          }\n          <div class=\"org-h-children\">\n            @for (child of current?.children; track child.id) {\n              <div\n                ui-org-chart-node\n                [node]=\"child\"\n                [depth]=\"depth + 1\"\n                [direction]=\"direction\"\n                [showConnectors]=\"showConnectors\"\n                [isExpanded]=\"isExpanded\"\n                [toggle]=\"toggle\"\n                [renderNode]=\"renderNode\"\n                (nodeClick)=\"nodeClick.emit($event)\"\n              ></div>\n            }\n          </div>\n        }\n      </div>\n    }\n  `,\n})\nexport class UiOrgChartNodeComponent {\n  @Input({ required: true }) node!: OrgNode\n  /** `node` may still be unset on first render; the template reads it through this. */\n  get current(): OrgNode | undefined {\n    return this.node\n  }\n  @Input() depth = 0\n  @Input({ transform: booleanAttribute }) isRoot = false\n  @Input() direction: OrgChartDirection = 'top-down'\n  @Input({ transform: booleanAttribute }) showConnectors = true\n  @Input() isExpanded: (node: OrgNode) => boolean = () => true\n  @Input() toggle: (node: OrgNode) => void = () => {}\n  @Input() renderNode?: TemplateRef<OrgNodeContext> | null\n  /** React `onNodeClick`. */\n  @Output() nodeClick = new EventEmitter<OrgNode>()\n\n  get open(): boolean {\n    return this.node ? this.isExpanded(this.node) : false\n  }\n  get hasChildren(): boolean {\n    return !!this.node?.children?.length\n  }\n  get isHorizontal(): boolean {\n    return this.direction === 'left-right'\n  }\n  get isOnlyChild(): boolean {\n    return (this.node?.children?.length ?? 0) <= 1\n  }\n  get initials(): string {\n    return this.node ? initials(this.node.name) : ''\n  }\n  get cardLabel(): string {\n    if (!this.node) return ''\n    return this.node.title ? `${this.node.name}, ${this.node.title}` : this.node.name\n  }\n  get cardClass(): string {\n    return cn(\n      'group relative flex w-52 cursor-pointer flex-col rounded-lg border p-3 shadow-xs transition-colors',\n      'border-border bg-card hover:bg-accent/50 focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none',\n      this.isRoot && 'ring-2 ring-primary/20',\n    )\n  }\n\n  onClick(): void {\n    this.nodeClick.emit(this.node)\n  }\n\n  onToggle(event: MouseEvent): void {\n    event.stopPropagation()\n    if (this.hasChildren) this.toggle(this.node)\n  }\n\n  onCardKeyDown(event: KeyboardEvent): void {\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      this.onClick()\n    }\n  }\n}\n\n/**\n * Angular port of UIPKGE OrganizationChart (React parity): renders the `data` tree\n * top-down or left-right with connector lines, expand / collapse per branch (all\n * expanded, or only the root with `defaultExpanded=false`), an optional zoom +\n * expand/collapse-all toolbar, `nodeClick` / `toggle` outputs, and a `renderNode`\n * template (`<ng-template let-node>`) rendered inside each card. `expandAll`,\n * `collapseAll`, `zoomIn`, `zoomOut` and `resetZoom` are public (React's ref handle).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-organization-chart, [ui-organization-chart]',\n  standalone: true,\n  encapsulation: ViewEncapsulation.None,\n  styles: [ORG_CHART_CSS],\n  imports: [UiOrgChartNodeComponent],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"organization-chart\"',\n    '[attr.data-direction]': 'direction',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (zoomable) {\n      <div 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\">{{ zoomPercent }}%</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            aria-label=\"Expand all\"\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            aria-label=\"Collapse all\"\n            (click)=\"collapseAll()\"\n          >\n            Collapse all\n          </button>\n        </div>\n      </div>\n    }\n    <div class=\"overflow-auto p-4\">\n      <div [style.transform]=\"'scale(' + zoom() + ')'\" class=\"origin-top transition-transform duration-200\">\n        @if (data) {\n          <div\n            ui-org-chart-node\n            [node]=\"data\"\n            [depth]=\"0\"\n            [isRoot]=\"true\"\n            [direction]=\"direction\"\n            [showConnectors]=\"showConnectors\"\n            [isExpanded]=\"isExpandedFn\"\n            [toggle]=\"toggleFn\"\n            [renderNode]=\"renderNode\"\n            (nodeClick)=\"nodeClick.emit($event)\"\n          ></div>\n        }\n      </div>\n    </div>\n  `,\n})\nexport class UiOrganizationChartComponent implements OnChanges {\n  @Input({ required: true }) data!: OrgNode\n  @Input() direction: OrgChartDirection = 'top-down'\n  @Input({ transform: booleanAttribute }) defaultExpanded = true\n  @Input({ transform: booleanAttribute }) showConnectors = true\n  @Input({ transform: booleanAttribute }) zoomable = false\n  /** Rendered inside each card below the name (React `renderNode`). */\n  @Input() renderNode?: TemplateRef<OrgNodeContext> | null\n  @Input('class') className?: string\n  /** React `onNodeClick`. */\n  @Output() nodeClick = new EventEmitter<OrgNode>()\n  /** React `onToggle(node, expanded)`. */\n  @Output() toggle = new EventEmitter<OrgChartToggleEvent>()\n\n  readonly expanded = signal<Set<string>>(new Set())\n  readonly zoom = signal(1)\n\n  readonly isExpandedFn = (node: OrgNode): boolean => this.expanded().has(node.id)\n  readonly toggleFn = (node: OrgNode): void => this.toggleNode(node)\n\n  get hostClass(): string {\n    return cn('block', organizationChartVariants(), this.className)\n  }\n\n  get zoomPercent(): number {\n    return Math.round(this.zoom() * 100)\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    // Re-sync expanded state when data or defaultExpanded changes.\n    if (changes['data'] || changes['defaultExpanded']) this.resetToDefault()\n  }\n\n  private resetToDefault(): void {\n    if (!this.data) return\n    this.expanded.set(this.defaultExpanded ? new Set(collectIds(this.data)) : new Set([this.data.id]))\n  }\n\n  toggleNode(node: OrgNode): void {\n    const next = new Set(this.expanded())\n    if (next.has(node.id)) next.delete(node.id)\n    else next.add(node.id)\n    this.expanded.set(next)\n    this.toggle.emit({ node, expanded: next.has(node.id) })\n  }\n\n  expandAll(): void {\n    this.expanded.set(new Set(collectIds(this.data)))\n  }\n\n  collapseAll(): void {\n    this.expanded.set(new Set([this.data.id]))\n  }\n\n  zoomIn(): void {\n    this.zoom.update((z) => Math.min(2, z + 0.1))\n  }\n\n  zoomOut(): void {\n    this.zoom.update((z) => Math.max(0.5, z - 0.1))\n  }\n\n  resetZoom(): void {\n    this.zoom.set(1)\n  }\n}\n\nexport { organizationChartVariants, type OrganizationChartVariants, type OrgNode }\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/organization-chart/organization-chart.component.ts"
    },
    {
      "path": "packages/registry-angular/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": "~/src/app/components/ui/organization-chart/organization-chart.variants.ts"
    },
    {
      "path": "packages/registry-angular/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": "~/src/app/components/ui/organization-chart/types.ts"
    },
    {
      "path": "packages/registry-angular/components/organization-chart/index.ts",
      "content": "export {\n  UiOrganizationChartComponent,\n  UiOrgChartNodeComponent,\n  UiOrgChartNodeOutletDirective,\n  organizationChartVariants,\n  type OrganizationChartVariants,\n  type OrgNode,\n  type OrgNodeContext,\n  type OrgChartDirection,\n  type OrgChartToggleEvent,\n} from './organization-chart.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/organization-chart/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "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 renderNode template.",
  "categories": [
    "display",
    "data"
  ]
}