{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree",
  "title": "Tree",
  "description": "A hierarchy set as a contents page: names on hairline branches, each hanging from under its folder's first letter, a folder opened at a time.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/collapsible.json",
    "https://thedirectors.agency/ui/r/item.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/tree.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Collapsible, CollapsibleContent, CollapsibleList, CollapsibleTrigger } from \"@/components/ui/collapsible\"\nimport { Item, ItemActions, ItemTitle } from \"@/components/ui/item\"\n\ntype TreeNode = {\n  id: string\n  label: React.ReactNode\n  /** A folder holds children; a node without them is a leaf. An empty array is an empty folder. */\n  children?: TreeNode[]\n  /** Set at the end of the line after a dotted leader, as a contents page sets its page numbers: a size, a count, a folio. */\n  value?: React.ReactNode\n  /** A leaf that goes somewhere is a link. */\n  href?: string\n}\n\ntype TreeProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  nodes: TreeNode[]\n  /**\n   * branch: a hairline spine hangs from under each open folder's first letter and every name sits on it at a dot; a\n   * closed folder is a ring, an open one a filled dot, the chosen leaf the accent. outline: the book's decimal\n   * contents, each name numbered by its place (1, 1.2, 1.2.3) in a pencil column, and no lines at all.\n   */\n  variant?: \"branch\" | \"outline\"\n  /** The chosen leaf's id, controlled or not. */\n  selected?: string | null\n  defaultSelected?: string | null\n  onSelectedChange?: (id: string) => void\n  /** The open folders' ids, controlled or not. */\n  expanded?: string[]\n  defaultExpanded?: string[]\n  onExpandedChange?: (ids: string[]) => void\n  /** A folder longer than this shows its first rows, then \"and 6 more\", as a collapsible does. */\n  limit?: number\n}\n\ntype Ctx = { selected: string | null; select: (id: string) => void; open: Set<string>; toggle: (id: string, open: boolean) => void; limit: number; touched: React.RefObject<boolean> }\nconst TreeCtx = React.createContext<Ctx | null>(null)\n\nconst CONTROLS = \"summary, .ot-tree-name\"\n\n/**\n * A hierarchy you open a folder at a time: native disclosures, so every folder opens with Enter or Space and Tab\n * walks it. The arrows walk it too, as a tree: up and down through what shows, right to open or step in, left to\n * close or step out, Home and End to the ends.\n */\nfunction Tree({ nodes, variant = \"branch\", selected, defaultSelected = null, onSelectedChange, expanded, defaultExpanded = [], onExpandedChange, limit = Infinity, className, onKeyDown, ...props }: TreeProps) {\n  const [ownSelected, setOwnSelected] = React.useState(defaultSelected)\n  const [ownOpen, setOwnOpen] = React.useState(() => new Set(defaultExpanded))\n  const touched = React.useRef(false) // a folder that opens before anyone touches the tree (open from the start) doesn't play its arrival\n  const open = React.useMemo(() => (expanded ? new Set(expanded) : ownOpen), [expanded, ownOpen])\n\n  const ctx: Ctx = {\n    selected: selected === undefined ? ownSelected : selected,\n    select(id) {\n      if (selected === undefined) setOwnSelected(id)\n      onSelectedChange?.(id)\n    },\n    open,\n    toggle(id, now) {\n      if (open.has(id) === now) return\n      const next = new Set(open)\n      if (now) next.add(id)\n      else next.delete(id)\n      if (!expanded) setOwnOpen(next)\n      onExpandedChange?.([...next])\n    },\n    limit,\n    touched,\n  }\n\n  function walk(e: React.KeyboardEvent<HTMLDivElement>) {\n    onKeyDown?.(e)\n    const at = e.target as HTMLElement\n    if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || !at.matches(CONTROLS)) return\n    const all = [...e.currentTarget.querySelectorAll<HTMLElement>(CONTROLS)].filter((el) => el.checkVisibility())\n    const i = all.indexOf(at)\n    const rtl = getComputedStyle(at).direction === \"rtl\"\n    const key = rtl ? ({ ArrowLeft: \"ArrowRight\", ArrowRight: \"ArrowLeft\" } as Record<string, string>)[e.key] ?? e.key : e.key\n    const folder = at.localName === \"summary\" ? (at.parentElement as HTMLDetailsElement) : null\n    let to: HTMLElement | null | undefined\n    if (key === \"ArrowDown\") to = all[i + 1]\n    else if (key === \"ArrowUp\") to = all[i - 1]\n    else if (key === \"Home\") to = all[0]\n    else if (key === \"End\") to = all.at(-1)\n    else if (key === \"ArrowRight\" && folder) {\n      if (!folder.open) folder.open = true\n      else to = all[i + 1]\n    } else if (key === \"ArrowLeft\") {\n      if (folder?.open) folder.open = false\n      else to = (folder ?? at).parentElement?.closest(\"details\")?.querySelector<HTMLElement>(\":scope > summary\")\n    } else return\n    e.preventDefault()\n    to?.focus()\n  }\n\n  return (\n    <TreeCtx.Provider value={ctx}>\n      <div\n        data-slot=\"tree\"\n        data-variant={variant === \"branch\" ? undefined : variant}\n        className={cn(\"ot-tree\", className)}\n        onKeyDown={walk}\n        onPointerDownCapture={() => void (touched.current = true)}\n        onKeyDownCapture={() => void (touched.current = true)}\n        {...props}\n      >\n        <Branch nodes={nodes} />\n      </div>\n    </TreeCtx.Provider>\n  )\n}\n\nfunction Branch({ nodes }: { nodes: TreeNode[] }) {\n  const { limit } = React.useContext(TreeCtx)!\n  const cut = nodes.length > limit ? Math.max(1, limit) : nodes.length\n  const rest = nodes.slice(cut)\n  return (\n    <ul className=\"ot-tree-list\">\n      {nodes.slice(0, cut).map((n) => (\n        <Node key={n.id} node={n} />\n      ))}\n      {rest.length ? (\n        <li className=\"ot-tree-tail\">\n          <Collapsible>\n            <CollapsibleTrigger openLabel={`Hide these ${rest.length}`}>and {rest.length} more</CollapsibleTrigger>\n            <CollapsibleContent>\n              <CollapsibleList>\n                {rest.map((n) => (\n                  <Node key={n.id} node={n} />\n                ))}\n              </CollapsibleList>\n            </CollapsibleContent>\n          </Collapsible>\n        </li>\n      ) : null}\n    </ul>\n  )\n}\n\nfunction Node({ node }: { node: TreeNode }) {\n  const { selected, select, open, toggle, touched } = React.useContext(TreeCtx)!\n  const [arriving, setArriving] = React.useState(false)\n  const end = node.value === undefined ? null : <ItemActions>{node.value}</ItemActions>\n\n  if (!node.children) {\n    const current = selected === node.id\n    const name = <ItemTitle>{node.label}</ItemTitle>\n    return (\n      <Item className=\"ot-tree-node ot-tree-row\" data-kind=\"leaf\" data-selected={current ? \"\" : undefined}>\n        {node.href ? (\n          <a className=\"ot-tree-name\" href={node.href} aria-current={current ? \"page\" : undefined} onClick={() => select(node.id)}>\n            {name}\n          </a>\n        ) : (\n          <button type=\"button\" className=\"ot-tree-name\" aria-current={current ? \"true\" : undefined} onClick={() => select(node.id)}>\n            {name}\n          </button>\n        )}\n        {end}\n      </Item>\n    )\n  }\n\n  const n = node.children.length\n  return (\n    <li className=\"ot-tree-node\" data-kind=\"folder\">\n      <details\n        className=\"ot-tree-folder\"\n        open={open.has(node.id)}\n        data-arriving={arriving ? \"\" : undefined}\n        onToggle={(e) => {\n          if (e.target !== e.currentTarget) return // a tail's toggle inside bubbles up\n          const now = e.currentTarget.open\n          setArriving(now && touched.current)\n          toggle(node.id, now)\n        }}\n      >\n        <summary className=\"ot-tree-row\">\n          <ItemTitle>\n            {node.label}\n            <sup className=\"ot-tree-count\" aria-hidden=\"true\">{n}</sup>\n            <span className=\"ot-sr\">, {n === 1 ? \"1 item\" : `${n} items`}</span>\n          </ItemTitle>\n          {end}\n        </summary>\n        {n ? <Branch nodes={node.children} /> : <p className=\"ot-tree-empty\">Empty</p>}\n      </details>\n    </li>\n  )\n}\n\nexport { Tree, type TreeProps, type TreeNode }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/tree.css",
      "content": "@layer components {\n  /* ── ot-tree ── a hierarchy set as a contents page: names on hairline branches. Each open folder's branch hangs from under the\n     first letter of its name, and every name sits on it at a dot, so the depth is read off the lines, not from boxes or icons.\n     A leaf is a small graphite dot, a closed folder a ring, an open one a filled dot; the chosen leaf is the one accent and\n     its name turns italic. A value (a size, a folio) is joined to its name by the item's leader. */\n  .ot-tree {\n    --ot-tree-pad: var(--ot-space-2); /* a row's padding above and below its name */\n    --ot-tree-indent: var(--ot-space-5); /* from a dot to its name, and so from a branch to the next one in */\n    --d: 7px;\n    padding-inline-start: calc(var(--d) / 2); min-width: 0;\n    font-size: var(--ot-p); line-height: var(--ot-p-lh); letter-spacing: var(--ot-p-tr);\n  }\n  .ot-tree-list { list-style: none; margin: 0; padding: 0; }\n  .ot-tree-list .ot-tree-list { padding-inline-start: var(--ot-tree-indent); }\n\n  .ot-tree .ot-tree-node { position: relative; padding: 0; border: 0; }\n  .ot-tree .ot-tree-row { position: relative; display: flex; align-items: baseline; gap: var(--ot-space-4); padding-block: var(--ot-tree-pad); padding-inline-start: var(--ot-tree-indent); border: 0; }\n  .ot-tree-folder > summary { list-style: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }\n  .ot-tree-folder > summary::-webkit-details-marker { display: none; }\n\n  /* The branch: each row carries its own length of it, the last one stopping at its dot. */\n  .ot-tree-node::before, .ot-tree-tail::before {\n    content: \"\"; position: absolute; inset-block: 0; inset-inline-start: calc(var(--ot-hairline) / -2); inline-size: var(--ot-hairline);\n    background: var(--ot-rule-strong); transform-origin: top;\n  }\n  :is(.ot-tree-list, .ot-tree .ot-collapse-list) > :is(.ot-tree-node, .ot-tree-tail):last-child::before { inset-block-end: auto; block-size: calc(var(--ot-tree-pad) + 0.5lh); }\n  .ot-tree > .ot-tree-list > :first-child::before { inset-block-start: calc(var(--ot-tree-pad) + 0.5lh); } /* the root's branch starts at its first name */\n  .ot-tree > .ot-tree-list > :only-child::before { content: none; }\n  .ot-tree-list > .ot-tree-tail:has(> .ot-collapse[open])::before { block-size: calc(2 * var(--ot-tree-pad) + 1lh); }\n\n  /* The dots */\n  .ot-tree-row::after {\n    content: \"\"; position: absolute; box-sizing: border-box; inset-inline-start: calc(var(--d) / -2); inset-block-start: calc(var(--ot-tree-pad) + 0.5lh - var(--d) / 2);\n    inline-size: var(--d); block-size: var(--d); border-radius: 50%; border: var(--ot-hairline) solid var(--c, var(--ot-graphite)); background: var(--c, var(--ot-graphite));\n    scale: 0.6; transition: scale var(--ot-moderato) var(--ot-spiccato), background-color var(--ot-allegro), border-color var(--ot-allegro);\n  }\n  .ot-tree-folder > .ot-tree-row::after { --c: var(--ot-ink); scale: 1; background: var(--ot-paper); }\n  .ot-tree-folder[open] > .ot-tree-row::after { background: var(--ot-ink); }\n  .ot-tree-row[data-selected]::after { --c: var(--ot-accent); scale: 1; }\n\n  /* The names: folders set firm, leaves in graphite that inks as you point; the chosen one italic, as yours. */\n  .ot-tree-name { appearance: none; margin: 0; padding: 0; border: 0; background: none; font: inherit; letter-spacing: inherit; color: var(--ot-graphite); text-align: start; text-decoration: none; cursor: pointer; transition: color var(--ot-allegro); }\n  .ot-tree-row:is(:hover, :focus-within, [data-force~=\"hover\"]) .ot-tree-name, .ot-tree-name[aria-current] { color: var(--ot-ink); }\n  .ot-tree-name[aria-current] .ot-item-title { font-style: italic; }\n  .ot-tree-folder > summary .ot-item-title { font-weight: 500; }\n  .ot-tree-count { margin-inline-start: 0.2em; font-size: var(--ot-pp); font-weight: 400; font-variant-numeric: tabular-nums; color: var(--ot-pencil); }\n  .ot-tree .ot-item-end { color: var(--ot-graphite); }\n  .ot-tree-folder > summary:is(:hover, :focus-visible, [data-force~=\"hover\"]) .ot-item-leader::after { mask-position: 0 0; mask-size: 100% 100%; }\n  .ot-tree-empty { margin: 0; padding-block: var(--ot-tree-pad); padding-inline-start: calc(2 * var(--ot-tree-indent)); font-size: var(--ot-pp); color: var(--ot-pencil); }\n\n  /* A long folder ends in the rest of itself, as the collapsible does: \"and 6 more\" stands on the branch's end. */\n  .ot-tree-tail { position: relative; }\n  .ot-tree .ot-tree-tail > .ot-collapse:is(*, [open]) > summary { --py: var(--ot-tree-pad); display: flex; inline-size: fit-content; margin-block: 0; margin-inline: var(--ot-tree-indent) 0; line-height: inherit; }\n  .ot-tree .ot-collapse-list > li { padding-block: 0; border: 0; }\n  .ot-tree .ot-tree-tail > .ot-collapse::details-content { overflow-clip-margin: var(--d); } /* the rest's dots and branch stand on its edge */\n  .ot-tree .ot-collapse-list > .ot-tree-row { padding-block: var(--ot-tree-pad); }\n\n  /* Opening a folder: its branch draws down and its names arrive in turn under it. Not for folders open from the start. */\n  .ot-tree-folder::details-content { block-size: 0; overflow: clip; transition: block-size var(--ot-andante) var(--ot-exhale), content-visibility var(--ot-andante) allow-discrete; }\n  .ot-tree-folder[open]::details-content { block-size: auto; }\n  .ot-tree-folder[data-arriving][open] > .ot-tree-list > li { animation: ot-arrive var(--ot-moderato) var(--ot-exhale) backwards; animation-delay: calc(sibling-index() * 2 * var(--ot-arpeggio)); }\n  .ot-tree-folder[data-arriving][open] > .ot-tree-list > li::before { animation: ot-draw-y var(--ot-andante) var(--ot-exhale) backwards; animation-delay: calc((sibling-index() - 1) * 2 * var(--ot-arpeggio)); }\n\n  /* ── outline ── the book's decimal contents: every name numbered by its place (1, 1.2, 1.2.3) in a pencil column, flush to one\n     edge at every depth, and no lines; the depth is in the numbers. An open folder's number inks; the chosen leaf's is the accent. */\n  .ot-tree[data-variant=\"outline\"] { --ot-tree-indent: calc(3em + var(--ot-space-4)); padding-inline-start: 0; }\n  .ot-tree[data-variant=\"outline\"] .ot-tree-list { padding-inline-start: 0; counter-reset: ot-tree; }\n  .ot-tree[data-variant=\"outline\"] .ot-tree-node { counter-increment: ot-tree; }\n  .ot-tree[data-variant=\"outline\"] :is(.ot-tree-node, .ot-tree-tail)::before { content: none; }\n  .ot-tree[data-variant=\"outline\"] .ot-tree-row { padding-inline-start: 0; }\n  .ot-tree[data-variant=\"outline\"] .ot-tree-row::after {\n    content: counters(ot-tree, \".\") / \"\"; position: static; order: -1; flex: none; inline-size: 3em; block-size: auto; scale: 1;\n    border: 0; border-radius: 0; background: none; font-variant-numeric: tabular-nums; color: var(--c, var(--ot-pencil)); transition: color var(--ot-allegro);\n  }\n  .ot-tree[data-variant=\"outline\"] .ot-tree-folder[open] > .ot-tree-row::after { --c: var(--ot-ink); }\n  .ot-tree[data-variant=\"outline\"] .ot-tree-row[data-selected]::after { --c: var(--ot-accent); }\n  .ot-tree[data-variant=\"outline\"] .ot-tree-list .ot-tree-list .ot-tree-folder > summary .ot-item-title { font-weight: 400; }\n  .ot-tree[data-variant=\"outline\"] > .ot-tree-list > .ot-tree-node > .ot-tree-folder > summary .ot-item-title { font-weight: 600; }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/tree.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/tree.css\"": {}
  },
  "meta": {
    "movement": "X",
    "underneath": "native",
    "contract": "ot-tree"
  },
  "type": "registry:ui"
}
