{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "orgchart-1",
  "title": "Org chart 1: organization tree",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An organization chart drawn as a real tree: connected person cards, a count toggle on every team, search that opens the path to a person, a details panel with manager and direct reports, and the standard tree keys.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/cover-art.json",
    "https://ui.ballmac.com/r/direction.json",
    "https://ui.ballmac.com/r/match-highlight.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/orgchart-1/orgchart-1.tsx",
      "content": "// Ballmac UI: Org Chart 1. https://ui.ballmac.com/blocks/orgchart-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { ChevronsDownUp, ChevronsUpDown, Mail, MapPin, Minus, Network, Search, UserRound, UsersRound } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { CoverArt } from \"@/components/ballmac/cover-art\"\nimport { MatchHighlight, matchesAll, queryTokens } from \"@/components/ballmac/match-highlight\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { useDirection } from \"@/lib/ballmac/direction\"\nimport { cn } from \"@/lib/utils\"\n\ntype Org1Person = {\n  id: string\n  name: string\n  title: string\n  /** The team, shown as a small label. */\n  team?: string\n  location?: string\n  email?: string\n  /** The people who report to this person. */\n  reports?: Org1Person[]\n}\n\ntype Org1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    /** The top of the organization. */\n    root?: Org1Person\n    title?: string\n    description?: string\n    /** How many levels are open at first. 1 shows only the top. */\n    defaultExpandedDepth?: number\n    /** The person whose details are open at first. */\n    defaultSelectedId?: string\n    /** Called when a person is chosen. */\n    onSelect?: (person: Org1Person) => void\n  }\n\nconst sample: Org1Person = {\n  id: \"mara\",\n  name: \"Mara Ellison\",\n  title: \"Chief Executive\",\n  location: \"Lisbon\",\n  email: \"mara@acme.com\",\n  reports: [\n    {\n      id: \"theo\",\n      name: \"Theo Nakamura\",\n      title: \"Chief Technology Officer\",\n      team: \"Engineering\",\n      location: \"Osaka\",\n      email: \"theo@acme.com\",\n      reports: [\n        { id: \"priya\", name: \"Priya Shah\", title: \"Head of Platform\", team: \"Engineering\", location: \"Lisbon\", email: \"priya@acme.com\", reports: [{ id: \"diego\", name: \"Diego Marín\", title: \"Staff Engineer\", team: \"Platform\", location: \"Madrid\" }, { id: \"kofi\", name: \"Kofi Mensah\", title: \"Senior Engineer\", team: \"Platform\", location: \"Accra\" }] },\n        { id: \"lena\", name: \"Lena Fischer\", title: \"Head of Product Engineering\", team: \"Engineering\", location: \"Berlin\", reports: [{ id: \"yuki\", name: \"Yuki Tanaka\", title: \"Engineer\", team: \"Web\", location: \"Tokyo\" }, { id: \"oliver\", name: \"Oliver Reed\", title: \"Engineer\", team: \"Desktop\", location: \"Leeds\" }] },\n      ],\n    },\n    {\n      id: \"noor\",\n      name: \"Noor Haddad\",\n      title: \"Head of Design\",\n      team: \"Design\",\n      location: \"Porto\",\n      email: \"noor@acme.com\",\n      reports: [\n        { id: \"amara\", name: \"Amara Okoye\", title: \"Product Designer\", team: \"Design\", location: \"Lagos\" },\n        { id: \"jordan\", name: \"Jordan Lee\", title: \"Design Systems Lead\", team: \"Design\", location: \"Lisbon\" },\n        { id: \"ines\", name: \"Ines Costa\", title: \"Researcher\", team: \"Design\", location: \"Porto\" },\n      ],\n    },\n    {\n      id: \"sam\",\n      name: \"Sam Okafor\",\n      title: \"Head of Customer Support\",\n      team: \"Support\",\n      location: \"Dublin\",\n      email: \"sam@acme.com\",\n      reports: [\n        { id: \"alex\", name: \"Alex Kim\", title: \"Support Lead\", team: \"Support\", location: \"Dublin\" },\n        { id: \"mia\", name: \"Mia Laurent\", title: \"Support Specialist\", team: \"Support\", location: \"Paris\" },\n      ],\n    },\n    { id: \"ana\", name: \"Ana Souza\", title: \"Head of Operations\", team: \"Operations\", location: \"Lisbon\", email: \"ana@acme.com\" },\n  ],\n}\n\nconst initials = (name: string) => name.split(\" \").map((w) => w[0]).join(\"\").slice(0, 2)\n\nfunction walk(p: Org1Person, depth = 0, parent?: string, out: Map<string, { person: Org1Person; parent?: string; depth: number }> = new Map()) {\n  out.set(p.id, { person: p, parent, depth })\n  p.reports?.forEach((r) => walk(r, depth + 1, p.id, out))\n  return out\n}\nconst countBelow = (p: Org1Person): number => (p.reports ?? []).reduce((n, r) => n + 1 + countBelow(r), 0)\n\nfunction OrgSkeleton() {\n  return (\n    <div className=\"grid justify-items-center gap-8 py-8\" aria-hidden=\"true\">\n      <Skeleton className=\"h-20 w-52 rounded-2xl\" />\n      <div className=\"flex flex-wrap justify-center gap-4\">\n        {[0, 1, 2].map((i) => (\n          <Skeleton key={i} className=\"h-20 w-52 max-w-full rounded-2xl\" />\n        ))}\n      </div>\n    </div>\n  )\n}\n\n/**\n * An organization chart built as a real tree: connected cards, a toggle on each team, search that opens the path to a person, a panel with\n * their details and the people who report to them, and the standard tree keys (arrows to move, Left and Right to collapse and expand, Enter to\n * choose). The connectors mirror in right-to-left.\n */\nfunction Org1({\n  root = sample,\n  title = \"Our organization\",\n  description = \"Who leads what, and who reports to whom. Select a person to see their team.\",\n  defaultExpandedDepth = 2,\n  defaultSelectedId,\n  onSelect,\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Org1Props) {\n  const id = React.useId()\n  const reduce = useReducedMotion()\n  const dir = useDirection()\n  const treeRef = React.useRef<HTMLUListElement>(null)\n  const scrollerRef = React.useRef<HTMLDivElement>(null)\n  const index = React.useMemo(() => (state === \"empty\" ? null : walk(root)), [root, state])\n  const [expanded, setExpanded] = React.useState<Set<string>>(() => new Set([...walk(root)].filter(([, v]) => v.depth < defaultExpandedDepth && v.person.reports?.length).map(([k]) => k)))\n  const [selectedId, setSelectedId] = React.useState<string | undefined>(defaultSelectedId ?? root.id)\n  const [activeId, setActiveId] = React.useState(root.id)\n  const [query, setQuery] = React.useState(\"\")\n  const [announce, setAnnounce] = React.useState(\"\")\n  const ready = state === \"ready\" || state === \"empty\"\n\n  const tokens = queryTokens(query)\n  const matches = index && tokens.length ? [...index.values()].filter((v) => matchesAll(`${v.person.name} ${v.person.title} ${v.person.team ?? \"\"}`, tokens)).map((v) => v.person) : []\n  const matchIds = new Set(matches.map((m) => m.id))\n  const selected = index && selectedId ? index.get(selectedId)?.person : undefined\n\n  // Open with the chosen person (or the top) in the middle of the view, not at the left edge of a wide tree.\n  React.useEffect(() => {\n    const center = () => {\n      const box = scrollerRef.current\n      const el = box?.querySelector<HTMLElement>(`[data-node=\"${defaultSelectedId ?? root.id}\"] > div`)\n      if (!box || !el) return\n      const b = box.getBoundingClientRect()\n      const r = el.getBoundingClientRect()\n      box.scrollLeft += r.left + r.width / 2 - (b.left + b.width / 2)\n      if (defaultSelectedId) box.scrollTop += Math.max(0, r.top - b.top - b.height / 2 + r.height / 2)\n    }\n    // Once now, and again after the direction and fonts have settled, so it holds in right-to-left too.\n    center()\n    const raf = requestAnimationFrame(center)\n    const later = setTimeout(center, 200)\n    return () => {\n      cancelAnimationFrame(raf)\n      clearTimeout(later)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [state, dir])\n\n  const focusNode = (nodeId: string) => {\n    setActiveId(nodeId)\n    requestAnimationFrame(() => {\n      const el = treeRef.current?.querySelector<HTMLElement>(`[data-node=\"${nodeId}\"]`)\n      el?.focus({ preventScroll: true })\n      el?.scrollIntoView({ block: \"nearest\", inline: \"center\", behavior: reduce ? \"auto\" : \"smooth\" })\n    })\n  }\n  const choose = (p: Org1Person) => {\n    setSelectedId(p.id)\n    onSelect?.(p)\n  }\n  const toggle = (nodeId: string, open?: boolean) =>\n    setExpanded((s) => {\n      const n = new Set(s)\n      const next = open ?? !n.has(nodeId)\n      if (next) n.add(nodeId)\n      else n.delete(nodeId)\n      return n\n    })\n  const reveal = (p: Org1Person) => {\n    if (!index) return\n    const next = new Set(expanded)\n    for (let at = index.get(p.id)?.parent; at; at = index.get(at)?.parent) next.add(at)\n    setExpanded(next)\n    choose(p)\n    focusNode(p.id)\n    setAnnounce(`Showing ${p.name}, ${p.title}`)\n  }\n\n  function onKeyDown(e: React.KeyboardEvent<HTMLElement>) {\n    const item = (e.target as HTMLElement).closest<HTMLElement>(\"[role=treeitem]\")\n    if (!item || e.target !== item || !index) return\n    const nodeId = item.dataset.node!\n    const info = index.get(nodeId)!\n    const items = [...(treeRef.current?.querySelectorAll<HTMLElement>(\"[role=treeitem]\") ?? [])]\n    const at = items.indexOf(item)\n    const open = expanded.has(nodeId)\n    const hasKids = !!info.person.reports?.length\n    const go = (el?: HTMLElement) => el && (e.preventDefault(), focusNode(el.dataset.node!))\n    if (e.key === \"ArrowDown\") go(items[at + 1])\n    else if (e.key === \"ArrowUp\") go(items[at - 1])\n    else if (e.key === \"Home\") go(items[0])\n    else if (e.key === \"End\") go(items[items.length - 1])\n    else if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\n      const forward = (e.key === \"ArrowRight\") !== (getComputedStyle(item).direction === \"rtl\")\n      e.preventDefault()\n      if (forward) {\n        if (hasKids && !open) {\n          toggle(nodeId, true)\n          setAnnounce(`${info.person.name}'s team opened`)\n        } else if (hasKids) focusNode(info.person.reports![0]!.id)\n      } else if (hasKids && open) {\n        toggle(nodeId, false)\n        setAnnounce(`${info.person.name}'s team closed`)\n      } else if (info.parent) focusNode(info.parent)\n    } else if (e.key === \"Enter\" || e.key === \" \") {\n      e.preventDefault()\n      choose(info.person)\n    }\n  }\n\n  const renderNode = (p: Org1Person, level: number, place: \"root\" | \"only\" | \"first\" | \"middle\" | \"last\"): React.ReactNode => {\n    const kids = p.reports ?? []\n    const open = expanded.has(p.id)\n    const below = countBelow(p)\n    const isSel = selectedId === p.id\n    return (\n      <li\n        key={p.id}\n        role=\"treeitem\"\n        data-node={p.id}\n        aria-level={level}\n        aria-expanded={kids.length ? open : undefined}\n        aria-selected={isSel}\n        aria-labelledby={`${id}-n-${p.id}`}\n        tabIndex={activeId === p.id ? 0 : -1}\n        onFocus={(e) => e.target === e.currentTarget && setActiveId(p.id)}\n        className={cn(\"relative flex flex-col items-center px-2 outline-none\", place !== \"root\" && \"pt-6\")}\n      >\n        {place !== \"root\" ? (\n          <>\n            <span aria-hidden=\"true\" className=\"bg-border absolute start-1/2 top-0 h-6 w-px\" />\n            {place !== \"only\" ? <span aria-hidden=\"true\" className={cn(\"bg-border absolute top-0 h-px\", place === \"first\" ? \"start-1/2 end-0\" : place === \"last\" ? \"start-0 end-1/2\" : \"inset-x-0\")} /> : null}\n          </>\n        ) : null}\n        <div\n          onClick={() => choose(p)}\n          className={cn(\n            \"bg-card relative w-56 cursor-pointer rounded-2xl border p-3 shadow-xs transition-[border-color,box-shadow] duration-150 hover:border-foreground/30 hover:shadow-md motion-reduce:transition-none\",\n            isSel && \"border-foreground ring-foreground ring-1\",\n            matchIds.has(p.id) && !isSel && \"ring-chart-3 border-chart-3 ring-2\",\n            \"[li:focus-visible>&]:ring-ring/60 [li:focus-visible>&]:ring-[3px]\"\n          )}\n        >\n          <div className=\"flex items-center gap-3\">\n            <span aria-hidden=\"true\" className=\"relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-full border text-xs font-semibold\">\n              <CoverArt seed={p.name} variant=\"mesh\" className=\"absolute inset-0\" />\n              <span className=\"relative\">{initials(p.name)}</span>\n            </span>\n            <div className=\"min-w-0 flex-1\">\n              <p id={`${id}-n-${p.id}`} className=\"truncate text-sm leading-tight font-semibold\">\n                <MatchHighlight text={p.name} query={tokens} />\n                <span className=\"sr-only\">, {p.title}{p.team ? `, ${p.team}` : \"\"}{kids.length ? `, ${below} ${below === 1 ? \"person\" : \"people\"} in their organization` : \"\"}</span>\n              </p>\n              <p aria-hidden=\"true\" className=\"text-muted-foreground mt-0.5 truncate text-xs\">\n                <MatchHighlight text={p.title} query={tokens} />\n              </p>\n            </div>\n          </div>\n          {p.team ? (\n            <p aria-hidden=\"true\" className=\"text-muted-foreground mt-2.5 flex items-center gap-1.5 text-[11px]\">\n              <UsersRound className=\"size-3\" />\n              {p.team}\n            </p>\n          ) : null}\n          {kids.length ? (\n            <button\n              type=\"button\"\n              tabIndex={-1}\n              aria-hidden=\"true\"\n              onClick={(e) => {\n                e.stopPropagation()\n                toggle(p.id)\n              }}\n              className={cn(\"bg-background hover:bg-accent absolute -bottom-3 start-1/2 z-10 flex h-6 min-w-6 -translate-x-1/2 items-center justify-center rounded-full border px-1.5 text-[11px] font-medium tabular-nums shadow-sm rtl:translate-x-1/2\")}\n            >\n              {open ? <Minus className=\"size-3\" /> : kids.length}\n            </button>\n          ) : null}\n        </div>\n        {kids.length && open ? (\n          <>\n            <span aria-hidden=\"true\" className=\"bg-border h-6 w-px\" />\n            <ul role=\"group\" className=\"flex justify-center\">\n              {kids.map((c, i) => renderNode(c, level + 1, kids.length === 1 ? \"only\" : i === 0 ? \"first\" : i === kids.length - 1 ? \"last\" : \"middle\"))}\n            </ul>\n          </>\n        ) : null}\n      </li>\n    )\n  }\n\n  const manager = selected && index ? index.get(selected.id)?.parent : undefined\n\n  const content =\n    !index ? (\n      <div className=\"bg-card rounded-2xl border px-6 py-16 text-center\">\n        <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-full\">\n          <Network className=\"size-6\" />\n        </span>\n        <h2 className=\"mt-5 text-lg font-semibold tracking-tight\">{emptyState?.title ?? \"No organization chart yet\"}</h2>\n        <p className=\"text-muted-foreground mx-auto mt-1.5 max-w-xs text-sm text-pretty\">{emptyState?.description ?? \"Add people and who they report to, and the chart draws itself.\"}</p>\n        {emptyState?.action}\n      </div>\n    ) : (\n      <div className=\"grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n        <div className=\"min-w-0\">\n          <div className=\"mb-4 flex flex-wrap items-center gap-2\">\n            <div className=\"bg-background focus-within:ring-ring/50 flex h-9 min-w-0 flex-1 basis-56 items-center gap-2 rounded-md border px-2.5 focus-within:ring-[3px]\">\n              <Search aria-hidden=\"true\" className=\"text-muted-foreground size-4\" />\n              <input type=\"search\" aria-label=\"Find a person\" placeholder=\"Find a person, a role or a team\" value={query} onChange={(e) => setQuery(e.target.value)} className=\"placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-sm outline-none\" />\n              {tokens.length ? <span className=\"text-muted-foreground text-xs tabular-nums\">{matches.length}</span> : null}\n            </div>\n            <Button variant=\"outline\" size=\"sm\" onClick={() => setExpanded(new Set([...index.values()].filter((v) => v.person.reports?.length).map((v) => v.person.id)))}>\n              <ChevronsUpDown aria-hidden=\"true\" />\n              Expand all\n            </Button>\n            <Button variant=\"outline\" size=\"sm\" onClick={() => setExpanded(new Set())}>\n              <ChevronsDownUp aria-hidden=\"true\" />\n              Collapse\n            </Button>\n          </div>\n          {tokens.length ? (\n            <ul aria-label=\"Matches\" className=\"mb-4 flex flex-wrap gap-2\">\n              {matches.slice(0, 6).map((m) => (\n                <li key={m.id}>\n                  <button type=\"button\" onClick={() => reveal(m)} className=\"hover:bg-accent focus-visible:ring-ring/50 rounded-full border px-3 py-1 text-xs outline-none focus-visible:ring-[3px]\">\n                    <MatchHighlight text={m.name} query={tokens} /> <span className=\"text-muted-foreground\">· {m.title}</span>\n                  </button>\n                </li>\n              ))}\n              {!matches.length ? <li className=\"text-muted-foreground text-sm\">Nobody matches “{query.trim()}”.</li> : null}\n            </ul>\n          ) : null}\n          <div ref={scrollerRef} tabIndex={-1} className=\"bg-card relative max-h-[40rem] overflow-auto rounded-2xl border p-6 pb-10\" onKeyDown={onKeyDown}>\n            <ul ref={treeRef} role=\"tree\" aria-label=\"Organization chart\" className=\"mx-auto flex w-max min-w-full justify-center\">\n              {renderNode(root, 1, \"root\")}\n            </ul>\n          </div>\n        </div>\n\n        <aside aria-labelledby={`${id}-person`} className=\"bg-card min-w-0 rounded-2xl border p-5 lg:sticky lg:top-6\">\n          {selected ? (\n            <>\n              <div className=\"flex items-center gap-4\">\n                <span aria-hidden=\"true\" className=\"relative flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-full border text-lg font-semibold\">\n                  <CoverArt seed={selected.name} variant=\"mesh\" className=\"absolute inset-0\" />\n                  <span className=\"relative\">{initials(selected.name)}</span>\n                </span>\n                <div className=\"min-w-0\">\n                  <h2 id={`${id}-person`} className=\"text-lg leading-tight font-semibold tracking-tight\">\n                    {selected.name}\n                  </h2>\n                  <p className=\"text-muted-foreground mt-0.5 text-sm text-pretty\">{selected.title}</p>\n                </div>\n              </div>\n              <dl className=\"mt-5 grid gap-2.5 text-sm\">\n                {selected.team ? (\n                  <div className=\"flex items-center gap-2.5\">\n                    <dt className=\"sr-only\">Team</dt>\n                    <UsersRound aria-hidden=\"true\" className=\"text-muted-foreground size-4 shrink-0\" />\n                    <dd>{selected.team}</dd>\n                  </div>\n                ) : null}\n                {selected.location ? (\n                  <div className=\"flex items-center gap-2.5\">\n                    <dt className=\"sr-only\">Location</dt>\n                    <MapPin aria-hidden=\"true\" className=\"text-muted-foreground size-4 shrink-0\" />\n                    <dd>{selected.location}</dd>\n                  </div>\n                ) : null}\n                {selected.email ? (\n                  <div className=\"flex items-center gap-2.5\">\n                    <dt className=\"sr-only\">Email</dt>\n                    <Mail aria-hidden=\"true\" className=\"text-muted-foreground size-4 shrink-0\" />\n                    <dd className=\"min-w-0 truncate\">\n                      <a href={`mailto:${selected.email}`} className=\"focus-visible:ring-ring/50 rounded-sm underline underline-offset-4 outline-none focus-visible:ring-[3px]\">\n                        {selected.email}\n                      </a>\n                    </dd>\n                  </div>\n                ) : null}\n              </dl>\n              {manager ? (\n                <div className=\"mt-5 border-t pt-4\">\n                  <p className=\"text-muted-foreground mb-2 text-xs font-medium\">Reports to</p>\n                  <button type=\"button\" onClick={() => reveal(index.get(manager)!.person)} className=\"hover:bg-accent focus-visible:ring-ring/50 -mx-2 flex w-[calc(100%+1rem)] items-center gap-2.5 rounded-lg px-2 py-1.5 text-start text-sm outline-none focus-visible:ring-[3px]\">\n                    <UserRound aria-hidden=\"true\" className=\"text-muted-foreground size-4\" />\n                    {index.get(manager)!.person.name}\n                    <span className=\"text-muted-foreground truncate text-xs\">{index.get(manager)!.person.title}</span>\n                  </button>\n                </div>\n              ) : null}\n              {selected.reports?.length ? (\n                <div className=\"mt-5 border-t pt-4\">\n                  <p className=\"text-muted-foreground mb-2 text-xs font-medium\">\n                    {selected.reports.length} direct {selected.reports.length === 1 ? \"report\" : \"reports\"}\n                    {countBelow(selected) > selected.reports.length ? `, ${countBelow(selected)} in the organization` : \"\"}\n                  </p>\n                  <ul className=\"grid gap-0.5\">\n                    {selected.reports.map((r) => (\n                      <li key={r.id}>\n                        <button type=\"button\" onClick={() => reveal(r)} className=\"hover:bg-accent focus-visible:ring-ring/50 -mx-2 flex w-[calc(100%+1rem)] items-center gap-2.5 rounded-lg px-2 py-1.5 text-start text-sm outline-none focus-visible:ring-[3px]\">\n                          <span className=\"truncate\">{r.name}</span>\n                          <span className=\"text-muted-foreground ms-auto truncate text-xs\">{r.title}</span>\n                        </button>\n                      </li>\n                    ))}\n                  </ul>\n                </div>\n              ) : null}\n            </>\n          ) : (\n            <p id={`${id}-person`} className=\"text-muted-foreground text-sm\">\n              Select a person to see their details.\n            </p>\n          )}\n        </aside>\n      </div>\n    )\n\n  return (\n    <section data-slot=\"orgchart-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-8 max-w-2xl\">\n        <h1 id={`${id}-title`} className=\"text-4xl font-semibold tracking-[-0.04em] text-balance\">\n          {title}\n        </h1>\n        <p className=\"text-muted-foreground mt-3 text-base text-pretty\">{description}</p>\n      </header>\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading the organization chart\" loading={<OrgSkeleton />} error={{ title: errorState?.title ?? \"The organization chart could not be loaded\", description: errorState?.description ?? \"Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </section>\n  )\n}\n\nexport { Org1, type Org1Props, type Org1Person }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/orgchart-1/orgchart-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "org chart",
      "organization",
      "hierarchy",
      "team",
      "people",
      "tree",
      "reports",
      "company",
      "about"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "An org chart. <Org1 root={{ id, name, title, team, location, email, reports: [...] }} defaultExpandedDepth={2} defaultSelectedId onSelect />. Pass one root person; everyone below hangs off reports. Search opens the path to the person; the side panel shows the manager and direct reports and lets you jump to them.",
      "whenToUse": [
        "An about or team page showing who reports to whom",
        "An internal directory with reporting lines"
      ],
      "whenNotToUse": [
        "A grid of team photos with no hierarchy (use team-1)",
        "A flat people directory with filters (use a table)"
      ],
      "composesWith": [
        "about-1",
        "team-1",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Tab, then Up / Down / Home / End",
          "action": "Move through the visible people in reading order (a roving tree)"
        },
        {
          "keys": "Right / Left",
          "action": "Open or close a team; Right on an open team goes to the first report, Left goes to the manager (swapped in right-to-left)"
        },
        {
          "keys": "Enter / Space",
          "action": "Choose a person and show their details"
        },
        {
          "keys": "Names",
          "action": "Each card is announced as name, role, team and how many people are in their organization"
        }
      ],
      "customization": [
        "defaultExpandedDepth: 1 shows only the top",
        "Connectors and the count toggle mirror in right-to-left",
        "Avatars are seeded art from the name, so they are identical on server and client"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "orgchart-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton cards under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No chart yet",
        "example": "orgchart-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Explains that the chart draws itself once people and reporting lines are added; emptyState sets the copy and can add a button."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "orgchart-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert with Try again (onRetry)."
      }
    ],
    "examples": [
      "orgchart-1-demo",
      "orgchart-1-open",
      "orgchart-1-loading",
      "orgchart-1-empty",
      "orgchart-1-error"
    ],
    "url": "https://ui.ballmac.com/components/orgchart-1"
  },
  "categories": [
    "blocks",
    "orgchart"
  ],
  "type": "registry:block"
}