{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "organization-chart",
  "title": "Organization Chart",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/organization-chart/OrganizationChart.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { OrgNode } from './types'\n\n  /** Imperative handle — grab it with `bind:this` (React `OrganizationChartHandle` parity). */\n  export interface OrganizationChartHandle {\n    expandAll: () => void\n    collapseAll: () => void\n    zoomIn: () => void\n    zoomOut: () => void\n    resetZoom: () => void\n  }\n\n  // `ontoggle` is omitted: the native ToggleEventHandler collides with the\n  // (node, expanded) toggle callback.\n  export interface OrganizationChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ontoggle'> {\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    /** Extra content rendered inside each node card. */\n    nodeSnippet?: Snippet<[{ node: OrgNode }]>\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { organizationChartVariants } from './organization-chart.variants'\n  import OrgChartNode from './OrgChartNode.svelte'\n\n  let {\n    class: className,\n    data,\n    direction = 'top-down',\n    defaultExpanded = true,\n    showConnectors = true,\n    zoomable = false,\n    onnodeclick,\n    ontoggle,\n    nodeSnippet,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: OrganizationChartProps = $props()\n\n  let expanded = $state<Set<string>>(new Set())\n\n  function 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\n  function defaultExpand() {\n    if (defaultExpanded) {\n      expanded = new Set(collectIds(data))\n    } else {\n      expanded = new Set([data.id])\n    }\n  }\n\n  // Mirror the Vue immediate watcher on [data, defaultExpanded].\n  $effect(() => {\n    data\n    defaultExpanded\n    defaultExpand()\n  })\n\n  function toggleNode(node: OrgNode) {\n    const next = new Set(expanded)\n    if (next.has(node.id)) next.delete(node.id)\n    else next.add(node.id)\n    expanded = next\n    ontoggle?.(node, next.has(node.id))\n  }\n\n  function isExpanded(node: OrgNode): boolean {\n    return expanded.has(node.id)\n  }\n\n  export function expandAll() {\n    expanded = new Set(collectIds(data))\n  }\n\n  export function collapseAll() {\n    expanded = new Set([data.id])\n  }\n\n  let zoom = $state(1)\n  export function zoomIn() {\n    zoom = Math.min(2, zoom + 0.1)\n  }\n  export function zoomOut() {\n    zoom = Math.max(0.5, zoom - 0.1)\n  }\n  export function resetZoom() {\n    zoom = 1\n  }\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"organization-chart\"\n  data-direction={direction}\n  class={cn(organizationChartVariants(), className)}\n  {...restProps}\n>\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        onclick={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        onclick={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        onclick={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          onclick={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          onclick={collapseAll}\n        >\n          Collapse all\n        </button>\n      </div>\n    </div>\n  {/if}\n  <div class=\"overflow-auto p-4\">\n    <div style:transform=\"scale({zoom})\" style:transform-origin=\"top center\" class=\"transition-transform duration-200\">\n      <OrgChartNode\n        node={data}\n        depth={0}\n        isRoot={true}\n        {direction}\n        {showConnectors}\n        {isExpanded}\n        toggle={toggleNode}\n        {onnodeclick}\n        nodeSnippet={nodeSnippet ?? children}\n      />\n    </div>\n  </div>\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/organization-chart/OrganizationChart.svelte"
    },
    {
      "path": "packages/registry-svelte/components/organization-chart/OrgChartNode.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { OrgNode } from './types'\n\n  export 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    /** Extra content rendered inside each node card. */\n    nodeSnippet?: Snippet<[{ node: OrgNode }]>\n    children?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { ChevronDown, ChevronRight } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  // Self-import for recursive rendering (`svelte:self` is deprecated).\n  import OrgChartNode from './OrgChartNode.svelte'\n\n  let {\n    node,\n    depth,\n    isRoot = false,\n    direction = 'top-down',\n    showConnectors = true,\n    isExpanded,\n    toggle,\n    onnodeclick,\n    nodeSnippet,\n    children,\n  }: OrgChartNodeProps = $props()\n\n  const open = $derived(isExpanded(node))\n  const hasChildren = $derived(!!node.children?.length)\n  const isHorizontal = $derived(direction === 'left-right')\n  const childCount = $derived(node.children?.length ?? 0)\n  const isOnlyChild = $derived(childCount <= 1)\n\n  function onClick() {\n    onnodeclick?.(node)\n  }\n\n  function onToggle(e: Event) {\n    e.stopPropagation()\n    if (hasChildren) toggle(node)\n  }\n\n  function onCardKeydown(e: KeyboardEvent) {\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      onClick()\n    }\n  }\n\n  function initials(name: string): string {\n    return name\n      .split(' ')\n      .map((p) => p[0])\n      .slice(0, 2)\n      .join('')\n      .toUpperCase()\n  }\n</script>\n\n{#snippet card()}\n  <div\n    role=\"button\"\n    tabindex=\"0\"\n    aria-label={node.title ? `${node.name}, ${node.title}` : node.name}\n    class={cn(\n      'bg-card hover:bg-accent/50 border-border group focus-visible:ring-ring relative flex w-52 cursor-pointer flex-col rounded-lg border p-3 shadow-xs transition-colors focus-visible:ring-2 focus-visible:outline-none',\n      isRoot ? 'ring-primary/20 ring-2' : '',\n    )}\n    onclick={onClick}\n    onkeydown={onCardKeydown}\n  >\n    <div class=\"flex items-center gap-2.5\">\n      {#if node.avatar}\n        <img src={node.avatar} alt={node.name} class=\"border-border size-10 shrink-0 rounded-full border object-cover\" />\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(node.name)}\n        </div>\n      {/if}\n      <div class=\"min-w-0 flex-1\">\n        <p class=\"truncate text-sm font-semibold\">{node.name}</p>\n        {#if node.title}\n          <p class=\"text-muted-foreground truncate text-xs\">{node.title}</p>\n        {/if}\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          aria-expanded={open}\n          aria-label={open ? 'Collapse' : 'Expand'}\n          onclick={onToggle}\n        >\n          {#if open}\n            <ChevronDown class=\"size-3.5\" aria-hidden=\"true\" />\n          {:else}\n            <ChevronRight class=\"size-3.5\" aria-hidden=\"true\" />\n          {/if}\n        </button>\n      {/if}\n    </div>\n    {#if nodeSnippet}\n      {@render nodeSnippet({ node })}\n    {:else}\n      {@render children?.()}\n    {/if}\n  </div>\n{/snippet}\n\n{#if !isHorizontal}\n  <!-- ══ Top-down (vertical) layout ══ -->\n  <div class=\"org-v\" data-root={isRoot ? '' : undefined}>\n    <!-- Node card -->\n    <div class=\"org-v-card\">\n      {@render card()}\n    </div>\n\n    <!-- Children -->\n    {#if hasChildren && open}\n      <div class=\"org-v-children\">\n        <!-- Vertical line from parent to horizontal sibling bar -->\n        {#if showConnectors}\n          <div class=\"org-v-line-down\"></div>\n        {/if}\n        <div class=\"org-v-children-row\" data-single={isOnlyChild ? '' : undefined}>\n          <!-- Horizontal bar connecting siblings (only for 2+ children) -->\n          {#if showConnectors && !isOnlyChild}\n            <div class=\"org-v-line-across\"></div>\n          {/if}\n          {#each node.children ?? [] as child (child.id)}\n            <OrgChartNode\n              node={child}\n              depth={depth + 1}\n              isRoot={false}\n              {direction}\n              {showConnectors}\n              {isExpanded}\n              {toggle}\n              {onnodeclick}\n              {nodeSnippet}\n            />\n          {/each}\n        </div>\n      </div>\n    {/if}\n  </div>\n{:else}\n  <!-- ══ Left-right (horizontal) layout ══ -->\n  <div class=\"org-h\" data-root={isRoot ? '' : undefined}>\n    <div class=\"flex items-start\">\n      <!-- Node card -->\n      <div class=\"org-h-card\">\n        {@render card()}\n      </div>\n\n      <!-- Children -->\n      {#if hasChildren && open}\n        {#if showConnectors}\n          <div class=\"org-h-line-right\"></div>\n        {/if}\n        <div class=\"org-h-children\">\n          {#each node.children ?? [] as child (child.id)}\n            <OrgChartNode\n              node={child}\n              depth={depth + 1}\n              isRoot={false}\n              {direction}\n              {showConnectors}\n              {isExpanded}\n              {toggle}\n              {onnodeclick}\n              {nodeSnippet}\n            />\n          {/each}\n        </div>\n      {/if}\n    </div>\n  </div>\n{/if}\n\n<style>\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": "~/src/lib/components/ui/organization-chart/OrgChartNode.svelte"
    },
    {
      "path": "packages/registry-svelte/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/lib/components/ui/organization-chart/organization-chart.variants.ts"
    },
    {
      "path": "packages/registry-svelte/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/lib/components/ui/organization-chart/types.ts"
    },
    {
      "path": "packages/registry-svelte/components/organization-chart/index.ts",
      "content": "export {\n  default as OrganizationChart,\n  type OrganizationChartHandle,\n  type OrganizationChartProps,\n} from './OrganizationChart.svelte'\nexport { default as OrgChartNode, type OrgChartNodeProps } from './OrgChartNode.svelte'\nexport { organizationChartVariants, type OrganizationChartVariants } from './organization-chart.variants'\nexport type { OrgNode } from './types'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/organization-chart/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte",
    "class-variance-authority"
  ],
  "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"
  ]
}