{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-tree",
  "title": "FileTree",
  "description": "Files and folders with change markers. Arrow keys walk the tree.",
  "registryDependencies": [
    "https://creance.pages.dev/r/creance-base.json"
  ],
  "files": [
    {
      "path": "file-tree/FileTree.css",
      "content": ".cr-tree,\n.cr-tree ul {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.cr-tree {\n  padding: var(--cr-space-2);\n  border-radius: var(--cr-radius-lg);\n  color: var(--cr-color-text-default);\n  font-family: var(--cr-font-family-sans);\n  font-size: var(--cr-font-size-md);\n}\n\n.cr-tree__item {\n  border-radius: var(--cr-radius-sm);\n  outline: none;\n}\n\n.cr-tree__row {\n  display: flex;\n  align-items: center;\n  gap: var(--cr-space-2);\n  height: 32px;\n  padding-inline-end: 10px;\n  border-radius: var(--cr-radius-sm);\n  cursor: pointer;\n}\n\n.cr-tree__row:hover {\n  background: var(--cr-color-bg-subtle);\n}\n\n.cr-tree__item[aria-selected='true'] > .cr-tree__row {\n  background: var(--cr-color-ink-subtle);\n  font-weight: var(--cr-font-weight-semibold);\n}\n\n.cr-tree__item:focus-visible > .cr-tree__row {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: -1px;\n}\n\n.cr-tree__twist {\n  display: flex;\n  width: 12px;\n  color: var(--cr-color-text-muted);\n  font-size: 12px;\n}\n\n.cr-tree__twist .cr-icon {\n  transition: transform var(--cr-motion-duration-fast) var(--cr-motion-easing-standard);\n}\n\n.cr-tree__item[aria-expanded='true'] > .cr-tree__row .cr-tree__twist .cr-icon {\n  transform: rotate(90deg);\n}\n\n/* Folder and file glyphs drawn with borders, so they follow the theme. */\n.cr-tree__icon {\n  box-sizing: border-box;\n  flex: none;\n  width: 14px;\n  height: 14px;\n  border: 1.5px solid var(--cr-color-text-muted);\n}\n\n.cr-tree__icon[data-kind='folder'] {\n  height: 11px;\n  border-radius: 2px 3px 3px 3px;\n}\n\n.cr-tree__icon[data-kind='file'] {\n  width: 11px;\n  border-radius: 2px;\n}\n\n.cr-tree__name {\n  flex: 1;\n  min-width: 0;\n  overflow: hidden;\n  font-family: var(--cr-font-family-mono);\n  font-size: var(--cr-font-size-sm);\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.cr-tree__status {\n  font-size: var(--cr-font-size-xs);\n  font-weight: var(--cr-font-weight-bold);\n}\n\n.cr-tree__status[data-status='D'] {\n  color: var(--cr-color-danger-text);\n}\n",
      "type": "registry:file",
      "target": "@components/creance/file-tree/FileTree.css"
    },
    {
      "path": "file-tree/FileTree.tsx",
      "content": "'use client';\n\nimport { useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';\nimport { cx } from '@/lib/creance/cx';\nimport { ChevronIcon } from '@/lib/creance/icons';\nimport './FileTree.css';\n\nexport interface FileNode {\n  id: string;\n  name: string;\n  /** Present for folders, even when empty. */\n  children?: FileNode[];\n  /** A short change marker: M modified, A added, D deleted. */\n  status?: 'M' | 'A' | 'D';\n}\n\nexport interface FileTreeProps {\n  nodes: FileNode[];\n  /** Names the tree, such as \"Changed files\". */\n  label: string;\n  defaultExpanded?: string[];\n  selected?: string | null;\n  onSelect?: (node: FileNode) => void;\n  className?: string;\n}\n\nconst statusWords = { M: 'modified', A: 'added', D: 'deleted' };\n\ninterface Row {\n  node: FileNode;\n  level: number;\n  parent: string | null;\n}\n\n/**\n * Files and folders as an ARIA tree. Up and Down move, Right opens a folder or steps in, Left closes\n * it or steps out, Home and End jump, Enter selects. One stop in the tab order.\n */\nexport function FileTree({ nodes, label, defaultExpanded = [], selected, onSelect, className }: FileTreeProps) {\n  const [expanded, setExpanded] = useState(() => new Set(defaultExpanded));\n  const [ownSelected, setOwnSelected] = useState<string | null>(null);\n  const current = selected === undefined ? ownSelected : selected;\n  const [focused, setFocused] = useState<string | null>(null);\n  const refs = useRef(new Map<string, HTMLLIElement>());\n\n  // The rows a person can see, in order: what the arrow keys walk.\n  const rows = useMemo(() => {\n    const out: Row[] = [];\n    const walk = (list: FileNode[], level: number, parent: string | null) =>\n      list.forEach((node) => {\n        out.push({ node, level, parent });\n        if (node.children && expanded.has(node.id)) walk(node.children, level + 1, node.id);\n      });\n    walk(nodes, 1, null);\n    return out;\n  }, [nodes, expanded]);\n\n  const active = focused ?? current ?? rows[0]?.node.id ?? null;\n  const focus = (id: string) => {\n    setFocused(id);\n    refs.current.get(id)?.focus();\n  };\n  const toggle = (id: string, open?: boolean) =>\n    setExpanded((prev) => {\n      const next = new Set(prev);\n      if (open ?? !next.has(id)) next.add(id);\n      else next.delete(id);\n      return next;\n    });\n  const select = (node: FileNode) => {\n    if (selected === undefined) setOwnSelected(node.id);\n    onSelect?.(node);\n  };\n\n  const onKeyDown = (e: KeyboardEvent, row: Row, index: number) => {\n    const { node } = row;\n    const folder = !!node.children;\n    const open = expanded.has(node.id);\n    const go: Record<string, () => void> = {\n      ArrowDown: () => rows[index + 1] && focus(rows[index + 1]!.node.id),\n      ArrowUp: () => rows[index - 1] && focus(rows[index - 1]!.node.id),\n      Home: () => focus(rows[0]!.node.id),\n      End: () => focus(rows[rows.length - 1]!.node.id),\n      ArrowRight: () => (folder && !open ? toggle(node.id, true) : folder && node.children!.length ? focus(node.children![0]!.id) : undefined),\n      ArrowLeft: () => (folder && open ? toggle(node.id, false) : row.parent ? focus(row.parent) : undefined),\n      Enter: () => (folder ? toggle(node.id) : select(node)),\n      ' ': () => (folder ? toggle(node.id) : select(node)),\n    };\n    if (go[e.key]) {\n      e.preventDefault();\n      e.stopPropagation();\n      go[e.key]!();\n    }\n  };\n\n  const render = (list: FileNode[]): ReactNode =>\n    list.map((node) => {\n      const index = rows.findIndex((r) => r.node.id === node.id);\n      const row = rows[index]!;\n      const folder = !!node.children;\n      const open = expanded.has(node.id);\n      return (\n        <li\n          key={node.id}\n          ref={(el) => {\n            if (el) refs.current.set(node.id, el);\n            else refs.current.delete(node.id);\n          }}\n          role=\"treeitem\"\n          aria-level={row.level}\n          aria-expanded={folder ? open : undefined}\n          aria-selected={folder ? undefined : node.id === current}\n          tabIndex={node.id === active ? 0 : -1}\n          className=\"cr-tree__item\"\n          onKeyDown={(e) => onKeyDown(e, row, index)}\n          onFocus={(e) => e.target === e.currentTarget && setFocused(node.id)}\n        >\n          {/* The row is drawn by this div; the li carries the tree semantics and focus. */}\n          {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}\n          <div\n            className=\"cr-tree__row\"\n            style={{ paddingInlineStart: `${(row.level - 1) * 20 + 10}px` }}\n            onClick={() => {\n              focus(node.id);\n              if (folder) toggle(node.id);\n              else select(node);\n            }}\n          >\n            <span className=\"cr-tree__twist\" aria-hidden=\"true\">\n              {folder && <ChevronIcon />}\n            </span>\n            <span className=\"cr-tree__icon\" aria-hidden=\"true\" data-kind={folder ? 'folder' : 'file'} />\n            <span className=\"cr-tree__name\">{node.name}</span>\n            {node.status && (\n              <>\n                <span className=\"cr-tree__status\" aria-hidden=\"true\" data-status={node.status}>\n                  {node.status}\n                </span>\n                <span className=\"cr-visually-hidden\">, {statusWords[node.status]}</span>\n              </>\n            )}\n          </div>\n          {folder && open && node.children!.length > 0 && <ul role=\"group\">{render(node.children!)}</ul>}\n        </li>\n      );\n    });\n\n  return (\n    <ul role=\"tree\" aria-label={label} className={cx('cr-tree cr-surface', className)}>\n      {render(nodes)}\n    </ul>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/file-tree/FileTree.tsx"
    }
  ],
  "categories": [
    "molecule",
    "data-display"
  ],
  "type": "registry:ui"
}