{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "organization-chart",
  "title": "Organization Chart",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/organization-chart/OrganizationChart.tsx",
      "content": "import * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport { organizationChartVariants } from './organization-chart.variants'\nimport { OrgChartNode } from './OrgChartNode'\nimport type { OrgNode } from './types'\nimport './organization-chart.css'\n\nexport interface OrganizationChartProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> {\n  data: OrgNode\n  direction?: 'top-down' | 'left-right'\n  defaultExpanded?: boolean\n  showConnectors?: boolean\n  zoomable?: boolean\n  onNodeClick?: (node: OrgNode) => void\n  onToggle?: (node: OrgNode, expanded: boolean) => void\n  renderNode?: (node: OrgNode) => React.ReactNode\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\nconst OrganizationChart = React.forwardRef<HTMLDivElement, OrganizationChartProps>(\n  (\n    {\n      data,\n      direction = 'top-down',\n      defaultExpanded = true,\n      showConnectors = true,\n      zoomable = false,\n      className,\n      onNodeClick,\n      onToggle,\n      renderNode,\n      ...props\n    },\n    ref,\n  ) => {\n    const [expanded, setExpanded] = React.useState<Set<string>>(() => {\n      if (defaultExpanded) return new Set(collectIds(data))\n      return new Set([data.id])\n    })\n    const [zoom, setZoom] = React.useState(1)\n\n    // Re-sync expanded state when data or defaultExpanded changes\n    React.useEffect(() => {\n      if (defaultExpanded) {\n        setExpanded(new Set(collectIds(data)))\n      } else {\n        setExpanded(new Set([data.id]))\n      }\n    }, [data, defaultExpanded])\n\n    function toggleNode(node: OrgNode) {\n      setExpanded((prev) => {\n        const next = new Set(prev)\n        if (next.has(node.id)) next.delete(node.id)\n        else next.add(node.id)\n        onToggle?.(node, next.has(node.id))\n        return next\n      })\n    }\n\n    function isExpanded(node: OrgNode): boolean {\n      return expanded.has(node.id)\n    }\n\n    function expandAll() {\n      setExpanded(new Set(collectIds(data)))\n    }\n\n    function collapseAll() {\n      setExpanded(new Set([data.id]))\n    }\n\n    function zoomIn() {\n      setZoom((z) => Math.min(2, z + 0.1))\n    }\n\n    function zoomOut() {\n      setZoom((z) => Math.max(0.5, z - 0.1))\n    }\n\n    function resetZoom() {\n      setZoom(1)\n    }\n\n    // Imperative handle mirroring Vue's defineExpose\n    React.useImperativeHandle(ref, () => ({\n      expandAll,\n      collapseAll,\n      zoomIn,\n      zoomOut,\n      resetZoom,\n    }))\n\n    const containerStyle = React.useMemo(\n      () => ({\n        transform: `scale(${zoom})`,\n        transformOrigin: 'top center',\n      }),\n      [zoom],\n    )\n\n    return (\n      <div\n        data-uipkge=\"\"\n        data-slot=\"organization-chart\"\n        data-direction={direction}\n        className={cn(organizationChartVariants(), className)}\n        {...props}\n      >\n        {zoomable && (\n          <div className=\"border-border flex items-center gap-2 border-b px-3 py-2\">\n            <button\n              type=\"button\"\n              className=\"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              onClick={zoomOut}\n            >\n              −\n            </button>\n            <span className=\"text-muted-foreground w-12 text-center text-xs tabular-nums\">\n              {Math.round(zoom * 100)}%\n            </span>\n            <button\n              type=\"button\"\n              className=\"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              onClick={zoomIn}\n            >\n              +\n            </button>\n            <button\n              type=\"button\"\n              className=\"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              onClick={resetZoom}\n            >\n              Reset\n            </button>\n            <div className=\"ml-auto flex gap-1\">\n              <button\n                type=\"button\"\n                className=\"text-muted-foreground hover:text-foreground hover:bg-accent rounded-md px-2 py-1 text-xs\"\n                onClick={expandAll}\n              >\n                Expand all\n              </button>\n              <button\n                type=\"button\"\n                className=\"text-muted-foreground hover:text-foreground hover:bg-accent rounded-md px-2 py-1 text-xs\"\n                onClick={collapseAll}\n              >\n                Collapse all\n              </button>\n            </div>\n          </div>\n        )}\n        <div className=\"overflow-auto p-4\">\n          <div style={containerStyle} className=\"transition-transform duration-200\">\n            <OrgChartNode\n              node={data}\n              depth={0}\n              isRoot\n              direction={direction}\n              showConnectors={showConnectors}\n              isExpanded={isExpanded}\n              toggle={toggleNode}\n              onNodeClick={onNodeClick}\n              renderNode={renderNode}\n            />\n          </div>\n        </div>\n      </div>\n    )\n  },\n)\nOrganizationChart.displayName = 'OrganizationChart'\n\nexport { OrganizationChart }\n",
      "type": "registry:ui",
      "target": "~/components/ui/organization-chart/OrganizationChart.tsx"
    },
    {
      "path": "packages/registry-react/components/organization-chart/OrgChartNode.tsx",
      "content": "import * as React from 'react'\nimport { ChevronDown, ChevronRight } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport type { OrgNode } from './types'\n\nexport interface OrgChartNodeProps {\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  onNodeClick?: (node: OrgNode) => void\n  renderNode?: (node: OrgNode) => React.ReactNode\n}\n\nfunction initials(name: string): string {\n  return name.split(' ').map((p) => p[0]).slice(0, 2).join('').toUpperCase()\n}\n\nconst OrgChartNode = React.forwardRef<HTMLDivElement, OrgChartNodeProps>(\n  (\n    { node, depth, isRoot = false, direction = 'top-down', showConnectors = true, isExpanded, toggle, onNodeClick, renderNode },\n    ref,\n  ) => {\n    const open = isExpanded(node)\n    const hasChildren = !!node.children?.length\n    const isHorizontal = direction === 'left-right'\n    const childCount = node.children?.length ?? 0\n    const isOnlyChild = childCount <= 1\n\n    function onClick() {\n      onNodeClick?.(node)\n    }\n\n    function onToggle(e: React.MouseEvent) {\n      e.stopPropagation()\n      if (hasChildren) toggle(node)\n    }\n\n    const cardClass = 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',\n      isRoot && 'ring-2 ring-primary/20',\n    )\n\n    const avatarBlock = (\n      <>\n        {node.avatar ? (\n          <img\n            src={node.avatar}\n            alt={node.name}\n            className=\"border-border size-10 shrink-0 rounded-full border object-cover\"\n          />\n        ) : (\n          <div className=\"bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-semibold\">\n            {initials(node.name)}\n          </div>\n        )}\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"truncate text-sm font-semibold\">{node.name}</p>\n          {node.title && <p className=\"text-muted-foreground truncate text-xs\">{node.title}</p>}\n        </div>\n        {hasChildren && (\n          <button\n            type=\"button\"\n            className=\"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            onClick={onToggle}\n          >\n            {open ? <ChevronDown className=\"size-3.5\" /> : <ChevronRight className=\"size-3.5\" />}\n          </button>\n        )}\n      </>\n    )\n\n    // ══ Top-down (vertical) layout ══\n    if (!isHorizontal) {\n      return (\n        <div ref={ref} className=\"org-v\" data-root={isRoot ? '' : undefined}>\n          {/* Node card */}\n          <div className=\"org-v-card\">\n            <div className={cardClass} onClick={onClick}>\n              <div className=\"flex items-center gap-2.5\">\n                {avatarBlock}\n              </div>\n              {renderNode?.(node)}\n            </div>\n          </div>\n\n          {/* Children */}\n          {hasChildren && open && (\n            <div className=\"org-v-children\">\n              {showConnectors && <div className=\"org-v-line-down\" />}\n              <div className=\"org-v-children-row\" data-single={isOnlyChild ? '' : undefined}>\n                {showConnectors && !isOnlyChild && <div className=\"org-v-line-across\" />}\n                {node.children!.map((child) => (\n                  <OrgChartNode\n                    key={child.id}\n                    node={child}\n                    depth={depth + 1}\n                    isRoot={false}\n                    direction={direction}\n                    showConnectors={showConnectors}\n                    isExpanded={isExpanded}\n                    toggle={toggle}\n                    onNodeClick={onNodeClick}\n                    renderNode={renderNode}\n                  />\n                ))}\n              </div>\n            </div>\n          )}\n        </div>\n      )\n    }\n\n    // ══ Left-right (horizontal) layout ══\n    return (\n      <div ref={ref} className=\"org-h\" data-root={isRoot ? '' : undefined}>\n        <div className=\"flex items-start\">\n          {/* Node card */}\n          <div className=\"org-h-card\">\n            <div className={cardClass} onClick={onClick}>\n              <div className=\"flex items-center gap-2.5\">\n                {avatarBlock}\n              </div>\n              {renderNode?.(node)}\n            </div>\n          </div>\n\n          {/* Children */}\n          {hasChildren && open && (\n            <>\n              {showConnectors && <div className=\"org-h-line-right\" />}\n              <div className=\"org-h-children\">\n                {node.children!.map((child) => (\n                  <OrgChartNode\n                    key={child.id}\n                    node={child}\n                    depth={depth + 1}\n                    isRoot={false}\n                    direction={direction}\n                    showConnectors={showConnectors}\n                    isExpanded={isExpanded}\n                    toggle={toggle}\n                    onNodeClick={onNodeClick}\n                    renderNode={renderNode}\n                  />\n                ))}\n              </div>\n            </>\n          )}\n        </div>\n      </div>\n    )\n  },\n)\nOrgChartNode.displayName = 'OrgChartNode'\n\nexport { OrgChartNode }\n",
      "type": "registry:ui",
      "target": "~/components/ui/organization-chart/OrgChartNode.tsx"
    },
    {
      "path": "packages/registry-react/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": "~/components/ui/organization-chart/organization-chart.variants.ts"
    },
    {
      "path": "packages/registry-react/components/organization-chart/organization-chart.css",
      "content": "/* ══ 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",
      "type": "registry:ui",
      "target": "~/components/ui/organization-chart/organization-chart.css"
    },
    {
      "path": "packages/registry-react/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": "~/components/ui/organization-chart/types.ts"
    },
    {
      "path": "packages/registry-react/components/organization-chart/index.ts",
      "content": "export { OrganizationChart, type OrganizationChartProps } from './OrganizationChart'\nexport { OrgChartNode, type OrgChartNodeProps } from './OrgChartNode'\nexport { organizationChartVariants, type OrganizationChartVariants } from './organization-chart.variants'\nexport type { OrgNode } from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/organization-chart/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "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 render prop.",
  "categories": [
    "display",
    "data"
  ]
}