{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "org-chart",
  "type": "registry:ui",
  "title": "Org chart",
  "description": "A tree by parent drawn as nested lists with connector lines: roots in a row, their reports in a row, deeper levels stacked. Cards drawn by the caller, optional drag and drop with the move check, an actions slot for the keyboard's way to every move.",
  "registryDependencies": [
    "@wibo/utils",
    "@wibo/org-tree"
  ],
  "files": [
    {
      "path": "src/components/org-chart.tsx",
      "type": "registry:ui",
      "content": "import { useEffect, useMemo, useRef, useState, type DragEvent, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  buildOrgTree,\n  canMoveInOrg,\n  orgMoveTargets,\n  orgStacksChildren,\n  type OrgTreeItem,\n  type OrgTreeNode,\n} from \"@/lib/org-tree\";\n\n/*\n * An org chart drawn as nested lists joined by hairlines, no chart library:\n * each item under its parent (a person under their manager), the roots side\n * by side at the top in name order. A root spreads its reports in a row and\n * every deeper level stacks under its manager on a rail, so a company of\n * twenty stays about one screen wide. The tree logic (roots, order, cycles,\n * the move check) is `@wibo/ui/lib/org-tree`.\n *\n * Generic: an item is `{ id, parentId }` and the caller draws each card's\n * body (`renderCard`) and its actions (`renderActions`, a `RowMenu`); the\n * chart holds no words, every label comes through props.\n *\n * Editing is opt-in (`editable`): a card dragged onto another card asks for\n * that parent (`onMove(id, parentId)`), onto the drop zone above the chart\n * for none (`onMove(id, null)`). A drop the move check refuses (onto the item\n * itself, into its own subtree, a line deeper than twelve) is never offered.\n * Dragging needs a pointer: every card is a Tab stop and its actions slot is\n * where the app offers the same moves by keyboard and screen reader (\"Change\n * manager\", \"Report to nobody\").\n *\n * The chart scrolls sideways inside its own box, never the page; a chart\n * wider than its box opens scrolled to its centre once. It holds state:\n * render it from client components.\n */\n\n/** What the chart knows about a card, for the caller's renderers. */\ninterface OrgChartCardState {\n  /** 0 for a card at the top. */\n  depth: number;\n  /** How many cards are drawn directly under this one. */\n  reports: number;\n  /** This card is being dragged. */\n  dragging: boolean;\n}\n\n/** The drop zone's words, required when the chart is editable. */\ninterface OrgChartLabels {\n  /** Shown while nothing is dragged: how to edit (\"Drag a card onto a person to change their manager\"). */\n  dropHint: string;\n  /** Shown while a card is dragged: what dropping here does (\"Drop here: reports to nobody\"). */\n  dropTop: string;\n}\n\ntype OrgChartEditing =\n  | { editable?: false; onMove?: never; labels?: never }\n  | {\n      editable: true;\n      /** A card was dropped on another card (`parentId`) or on the drop zone (`null`). Moves the check refuses never arrive. */\n      onMove: (id: string, parentId: string | null) => void;\n      labels: OrgChartLabels;\n    };\n\ntype OrgChartProps<T extends OrgTreeItem> = OrgChartEditing & {\n  /** Every item of the chart; an item whose parent is not among them is drawn at the top. */\n  nodes: readonly T[];\n  /** The body of an item's card: name, job title, badges, report count. */\n  renderCard: (node: T, state: OrgChartCardState) => ReactNode;\n  /** The card's actions, top right (a `RowMenu`): the keyboard's way to every move. */\n  renderActions?: (node: T, state: OrgChartCardState) => ReactNode;\n  /** Accessible name of the chart (\"Org chart\"). */\n  label: string;\n  /** Accessible name of a card, read when it takes the focus (\"Ada Lovelace, Head of Finance, 3 reports\"). */\n  cardLabel?: (node: T) => string;\n  /** The text items are ordered by (a person's name). Defaults to the id. */\n  sortKey?: (node: T) => string;\n  /** The locale the names are ordered in. */\n  locale?: string;\n  className?: string;\n};\n\nconst TOP = \"\\u0000top\";\n\nfunction OrgChart<T extends OrgTreeItem>({\n  nodes,\n  renderCard,\n  renderActions,\n  label,\n  cardLabel,\n  sortKey,\n  locale,\n  editable = false,\n  onMove,\n  labels,\n  className,\n}: OrgChartProps<T>) {\n  const [dragging, setDragging] = useState<string | null>(null);\n  const [over, setOver] = useState<string | null>(null);\n\n  const tree = useMemo(() => buildOrgTree(nodes, { sortKey, locale }), [nodes, sortKey, locale]);\n\n  /* While a card is dragged: where it may land, computed once per drag. */\n  const targets = useMemo(() => {\n    if (dragging === null) return null;\n    const ids = new Set(orgMoveTargets(nodes, dragging).map((node) => node.id));\n    ids.delete(nodes.find((node) => node.id === dragging)?.parentId ?? TOP);\n    return ids;\n  }, [dragging, nodes]);\n  const topAllowed = dragging !== null && canMoveInOrg(nodes, dragging, null) === \"ok\";\n\n  /* A chart wider than its box opens on its centre, once; after that the scroll is the reader's. */\n  const scroller = useRef<HTMLDivElement | null>(null);\n  const centred = useRef(false);\n  useEffect(() => {\n    const box = scroller.current;\n    if (box === null || centred.current || tree.length === 0) return;\n    centred.current = true;\n    box.scrollLeft = Math.max(0, (box.scrollWidth - box.clientWidth) / 2);\n  }, [tree]);\n\n  const endDrag = () => {\n    setDragging(null);\n    setOver(null);\n  };\n\n  const allow = (target: string | null) => (event: DragEvent) => {\n    if (dragging === null) return;\n    if (target === null ? !topAllowed : !targets?.has(target)) return;\n    event.preventDefault();\n    event.dataTransfer.dropEffect = \"move\";\n    setOver(target ?? TOP);\n  };\n\n  const leave = (target: string) => () => setOver((current) => (current === target ? null : current));\n\n  const drop = (target: string | null) => (event: DragEvent) => {\n    event.preventDefault();\n    const id = dragging;\n    endDrag();\n    if (id === null || !onMove || canMoveInOrg(nodes, id, target) !== \"ok\") return;\n    onMove(id, target);\n  };\n\n  const card = (node: OrgTreeNode<T>) => {\n    const { item } = node;\n    const state: OrgChartCardState = { depth: node.depth, reports: node.children.length, dragging: dragging === item.id };\n    const droppable = targets?.has(item.id) ?? false;\n    const actions = renderActions?.(item, state);\n    return (\n      <div\n        data-slot=\"org-chart-card\"\n        data-id={item.id}\n        data-drop-target={droppable || undefined}\n        data-over={over === item.id || undefined}\n        role=\"group\"\n        aria-label={cardLabel?.(item)}\n        tabIndex={0}\n        draggable={editable || undefined}\n        onDragStart={\n          editable\n            ? (event) => {\n                event.dataTransfer.setData(\"text/plain\", item.id);\n                event.dataTransfer.effectAllowed = \"move\";\n                setDragging(item.id);\n              }\n            : undefined\n        }\n        onDragEnd={editable ? endDrag : undefined}\n        onDragOver={editable ? allow(item.id) : undefined}\n        onDragLeave={editable ? leave(item.id) : undefined}\n        onDrop={editable ? drop(item.id) : undefined}\n        className={cn(\n          \"relative flex w-52 items-start gap-2 rounded-xl border border-border bg-card px-3 py-2.5 text-left text-card-foreground shadow-card outline-none\",\n          \"transition-[background-color,border-color,opacity] duration-150 ease-out\",\n          \"focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-1 focus-visible:ring-offset-background\",\n          editable && \"cursor-grab active:cursor-grabbing\",\n          state.dragging && \"opacity-50\",\n          droppable && \"border-dashed border-foreground/30\",\n          over === item.id && \"border-solid border-accent bg-accent/6\",\n        )}\n      >\n        <div className=\"flex min-w-0 flex-1 flex-col gap-1\">{renderCard(item, state)}</div>\n        {actions ? <div className=\"-mt-0.5 -mr-1.5 shrink-0\">{actions}</div> : null}\n      </div>\n    );\n  };\n\n  /* A stacked report carries its own reports stacked under it, nested; each draws its piece of the rail. */\n  const stack = (children: OrgTreeNode<T>[]) => (\n    <ul data-slot=\"org-chart-stack\" className=\"relative flex flex-col gap-2 pt-3 pl-8\">\n      {children.map((child, index) => {\n        const first = index === 0;\n        const last = index === children.length - 1;\n        return (\n          <li\n            key={child.item.id}\n            className={cn(\n              \"relative\",\n              /* The tick into the card. */\n              \"before:absolute before:top-[22px] before:-left-4 before:w-4 before:border-t before:border-input\",\n              /* The rail beside it, from the gap above to the next report (to the tick for the last). */\n              \"after:absolute after:-left-4 after:border-l after:border-input\",\n              first ? \"after:-top-3\" : \"after:-top-2\",\n              last ? (first ? \"after:h-[34px]\" : \"after:h-[30px]\") : \"after:bottom-0\",\n            )}\n          >\n            {card(child)}\n            {child.children.length > 0 ? stack(child.children) : null}\n          </li>\n        );\n      })}\n    </ul>\n  );\n\n  /* A row of reports under a root: a stem down from the root, a bar across, a drop into each card. */\n  const row = (children: OrgTreeNode<T>[]) => (\n    <ul\n      data-slot=\"org-chart-row\"\n      className=\"relative flex justify-center pt-5 before:absolute before:top-0 before:left-1/2 before:h-5 before:border-l before:border-input\"\n    >\n      {children.map((child, index) => {\n        const only = children.length === 1;\n        const first = index === 0;\n        const last = index === children.length - 1;\n        return (\n          <li\n            key={child.item.id}\n            className={cn(\n              \"relative flex flex-col items-center px-2\",\n              only\n                ? \"pt-0\"\n                : [\n                    \"pt-5\",\n                    /* The bar's left half, and the drop into the card for the last. */\n                    \"before:absolute before:top-0 before:right-1/2 before:h-5 before:w-1/2 before:border-input\",\n                    first ? \"before:border-0\" : \"before:border-t\",\n                    last && \"before:border-r before:rounded-tr-md\",\n                    /* The bar's right half with the drop into the card. */\n                    \"after:absolute after:top-0 after:left-1/2 after:h-5 after:w-1/2 after:border-input\",\n                    last ? \"after:border-0\" : \"after:border-t after:border-l\",\n                    first && \"after:rounded-tl-md\",\n                  ],\n            )}\n          >\n            {branch(child)}\n          </li>\n        );\n      })}\n    </ul>\n  );\n\n  function branch(node: OrgTreeNode<T>) {\n    return (\n      <>\n        {card(node)}\n        {node.children.length > 0 ? (orgStacksChildren(node) ? stack(node.children) : row(node.children)) : null}\n      </>\n    );\n  }\n\n  return (\n    <div data-slot=\"org-chart\" className={cn(\"flex min-w-0 flex-col gap-3\", className)}>\n      {editable && labels ? (\n        <div\n          data-slot=\"org-chart-drop-zone\"\n          data-over={over === TOP || undefined}\n          onDragOver={allow(null)}\n          onDragLeave={leave(TOP)}\n          onDrop={drop(null)}\n          className={cn(\n            \"rounded-lg border border-dashed border-input px-3 py-2 text-xs text-muted-foreground transition-colors duration-150 ease-out\",\n            topAllowed && \"border-foreground/30\",\n            over === TOP && \"border-solid border-accent bg-accent/6 text-foreground\",\n          )}\n        >\n          {dragging !== null ? labels.dropTop : labels.dropHint}\n        </div>\n      ) : null}\n      <div ref={scroller} data-slot=\"org-chart-scroller\" className=\"max-w-full overflow-x-auto overscroll-x-contain pb-2\">\n        <ul aria-label={label} className=\"flex min-w-max justify-center gap-6 px-2 pt-1 pb-2\">\n          {tree.map((node) => (\n            <li key={node.item.id} className=\"flex flex-col items-center\">\n              {branch(node)}\n            </li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  );\n}\n\nexport { OrgChart };\nexport type { OrgChartCardState, OrgChartLabels, OrgChartProps };\n"
    }
  ]
}
