{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "org-tree",
  "type": "registry:lib",
  "title": "Org tree",
  "description": "The org chart's pure tree logic: items with a parent id folded into a forest in name order (roots: no parent, a parent outside the set, or a cycle), the move check (onto itself, into its own subtree, deeper than twelve levels) and the parents a move may pick.",
  "files": [
    {
      "path": "src/lib/org-tree.ts",
      "type": "registry:lib",
      "content": "/*\n * The org chart's tree logic, pure and framework-free: items that name their\n * parent (a person and their manager) folded into a forest, the drawing rule\n * (which reports spread in a row, which stack under their manager), and the\n * move check a drag or a menu runs before it asks the app to change a line.\n * `OrgChart` draws with it; an app reads it too, for a \"Change manager\"\n * picker that offers only the moves the chart would accept.\n *\n * Ported from the Luminars console (`console/map/org.ts`):\n * - a root is an item with no parent, whose parent is not in the set, or\n *   whose chain of parents comes back to itself (a cycle is drawn, never\n *   followed, so a bad row cannot hang a page);\n * - roots and every item's children are in name order (`sortKey`, compared\n *   with `Intl.Collator`), ties broken by id;\n * - a move is refused onto the item itself, into its own subtree (a loop) and\n *   when the moved item's subtree would end deeper than `MAX_ORG_DEPTH` levels\n *   below the top (a root sits at depth 0).\n */\n\n/** One item of the chart: its id and its parent's id (null at the top). */\nexport interface OrgTreeItem {\n  id: string;\n  parentId: string | null;\n}\n\n/** An item placed in the forest. */\nexport interface OrgTreeNode<T extends OrgTreeItem> {\n  item: T;\n  /** 0 for a root. */\n  depth: number;\n  /** The item's children, in name order. */\n  children: OrgTreeNode<T>[];\n}\n\nexport interface OrgTreeOptions<T extends OrgTreeItem> {\n  /** The text items are ordered by (a person's name). Defaults to the id. */\n  sortKey?: (item: T) => string;\n  /** The locale of the collator that orders them. Defaults to the runtime's. */\n  locale?: string;\n}\n\n/** The deepest a line may run: depth 12 below a root, thirteen levels in all. */\nexport const MAX_ORG_DEPTH = 12;\n\n/**\n * Why a move is refused, or `ok`:\n * - `self`: onto the item itself;\n * - `same`: onto the parent it already has (nothing changes);\n * - `loop`: into its own subtree;\n * - `tooDeep`: its subtree would end deeper than `MAX_ORG_DEPTH`.\n */\nexport type OrgMoveVerdict = \"ok\" | \"self\" | \"same\" | \"loop\" | \"tooDeep\";\n\n/** The items by id (the first of a repeated id wins) and their children. */\ninterface OrgIndex<T extends OrgTreeItem> {\n  items: T[];\n  byId: Map<string, T>;\n  children: Map<string, T[]>;\n}\n\nfunction indexItems<T extends OrgTreeItem>(items: readonly T[]): OrgIndex<T> {\n  const byId = new Map<string, T>();\n  for (const item of items) if (!byId.has(item.id)) byId.set(item.id, item);\n  const unique = [...byId.values()];\n  const children = new Map<string, T[]>();\n  for (const item of unique) {\n    const parent = item.parentId;\n    if (parent === null || parent === item.id || !byId.has(parent)) continue;\n    const list = children.get(parent);\n    if (list) list.push(item);\n    else children.set(parent, [item]);\n  }\n  return { items: unique, byId, children };\n}\n\n/** The ids whose chain of parents comes back to themselves. */\nfunction cycleMembers<T extends OrgTreeItem>(index: OrgIndex<T>): Set<string> {\n  const onCycle = new Set<string>();\n  const done = new Set<string>();\n  for (const start of index.items) {\n    if (done.has(start.id)) continue;\n    const path: string[] = [];\n    const onPath = new Map<string, number>();\n    let at: string | null = start.id;\n    while (at !== null && index.byId.has(at) && !done.has(at)) {\n      const seenAt = onPath.get(at);\n      if (seenAt !== undefined) {\n        for (const id of path.slice(seenAt)) onCycle.add(id);\n        break;\n      }\n      onPath.set(at, path.length);\n      path.push(at);\n      at = index.byId.get(at)?.parentId ?? null;\n    }\n    for (const id of path) done.add(id);\n  }\n  return onCycle;\n}\n\nfunction comparator<T extends OrgTreeItem>(options: OrgTreeOptions<T>) {\n  const collator = new Intl.Collator(options.locale, { sensitivity: \"base\", numeric: true });\n  const key = options.sortKey ?? ((item: T) => item.id);\n  return (a: T, b: T) => collator.compare(key(a), key(b)) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);\n}\n\n/** The items as a forest: the roots in name order, each with its children in name order. */\nexport function buildOrgTree<T extends OrgTreeItem>(\n  items: readonly T[],\n  options: OrgTreeOptions<T> = {},\n): OrgTreeNode<T>[] {\n  const index = indexItems(items);\n  const cycles = cycleMembers(index);\n  const compare = comparator(options);\n  const roots = index.items.filter(\n    (item) =>\n      item.parentId === null ||\n      item.parentId === item.id ||\n      !index.byId.has(item.parentId) ||\n      cycles.has(item.id),\n  );\n  const placed = new Set<string>();\n  const build = (item: T, depth: number): OrgTreeNode<T> => {\n    placed.add(item.id);\n    const children = (index.children.get(item.id) ?? [])\n      .filter((child) => !cycles.has(child.id) && !placed.has(child.id))\n      .sort(compare);\n    return { item, depth, children: children.map((child) => build(child, depth + 1)) };\n  };\n  return roots.sort(compare).map((root) => build(root, 0));\n}\n\n/** Everybody under an item, at any depth. */\nexport function orgDescendants<T extends OrgTreeItem>(items: readonly T[], id: string): Set<string> {\n  return descendantsIn(indexItems(items), id);\n}\n\nfunction descendantsIn<T extends OrgTreeItem>(index: OrgIndex<T>, id: string): Set<string> {\n  const out = new Set<string>();\n  const queue = [id];\n  for (let at = queue.shift(); at !== undefined; at = queue.shift()) {\n    for (const child of index.children.get(at) ?? []) {\n      if (child.id === id || out.has(child.id)) continue;\n      out.add(child.id);\n      queue.push(child.id);\n    }\n  }\n  return out;\n}\n\n/** How many levels an item sits below the top: 0 for a root, or for an id not in the set. */\nexport function orgDepth<T extends OrgTreeItem>(items: readonly T[], id: string): number {\n  return depthIn(indexItems(items), id);\n}\n\nfunction depthIn<T extends OrgTreeItem>(index: OrgIndex<T>, id: string): number {\n  const seen = new Set<string>([id]);\n  let depth = 0;\n  let at = index.byId.get(id)?.parentId ?? null;\n  while (at !== null && index.byId.has(at) && !seen.has(at)) {\n    seen.add(at);\n    depth += 1;\n    at = index.byId.get(at)?.parentId ?? null;\n  }\n  return depth;\n}\n\n/** How many levels the subtree under an item spans: 0 for a leaf. */\nexport function orgHeight<T extends OrgTreeItem>(items: readonly T[], id: string): number {\n  return heightIn(indexItems(items), id);\n}\n\nfunction heightIn<T extends OrgTreeItem>(index: OrgIndex<T>, id: string): number {\n  let height = 0;\n  let level = [id];\n  const seen = new Set<string>([id]);\n  for (;;) {\n    const next: string[] = [];\n    for (const at of level) {\n      for (const child of index.children.get(at) ?? []) {\n        if (seen.has(child.id)) continue;\n        seen.add(child.id);\n        next.push(child.id);\n      }\n    }\n    if (next.length === 0) return height;\n    height += 1;\n    level = next;\n  }\n}\n\n/**\n * Whether `id` may be put under `parentId` (null: at the top). An id not in\n * the set (a person not saved yet) moves with no subtree of its own.\n */\nexport function canMoveInOrg<T extends OrgTreeItem>(\n  items: readonly T[],\n  id: string,\n  parentId: string | null,\n): OrgMoveVerdict {\n  const index = indexItems(items);\n  return verdictIn(index, id, parentId, () => descendantsIn(index, id), () => heightIn(index, id));\n}\n\nfunction verdictIn<T extends OrgTreeItem>(\n  index: OrgIndex<T>,\n  id: string,\n  parentId: string | null,\n  descendants: () => Set<string>,\n  height: () => number,\n): OrgMoveVerdict {\n  if (parentId === id) return \"self\";\n  if ((index.byId.get(id)?.parentId ?? null) === parentId) return \"same\";\n  if (parentId === null) return \"ok\";\n  if (descendants().has(parentId)) return \"loop\";\n  if (depthIn(index, parentId) + 1 + height() > MAX_ORG_DEPTH) return \"tooDeep\";\n  return \"ok\";\n}\n\n/**\n * The items `id` may be put under, in name order: every item the move check\n * accepts, and its current parent (verdict `same`), so a picker shows the\n * present choice among the others. Never the item itself or anybody in its\n * subtree, never a line deeper than `MAX_ORG_DEPTH`.\n */\nexport function orgMoveTargets<T extends OrgTreeItem>(\n  items: readonly T[],\n  id: string,\n  options: OrgTreeOptions<T> = {},\n): T[] {\n  const index = indexItems(items);\n  const below = descendantsIn(index, id);\n  const height = heightIn(index, id);\n  return index.items\n    .filter((target) => {\n      const verdict = verdictIn(index, id, target.id, () => below, () => height);\n      return verdict === \"ok\" || verdict === \"same\";\n    })\n    .sort(comparator(options));\n}\n\n/**\n * The drawing rule: a root spreads its reports in a row (the chart's one wide\n * level) unless none of them has reports of its own; every deeper level\n * stacks its reports under it, nested. This keeps a company of twenty on\n * about one screen.\n */\nexport function orgStacksChildren<T extends OrgTreeItem>(node: OrgTreeNode<T>): boolean {\n  if (node.children.length === 0) return false;\n  return node.depth >= 1 || node.children.every((child) => child.children.length === 0);\n}\n"
    }
  ]
}
