{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-view",
  "type": "registry:ui",
  "title": "TreeView",
  "description": "نمای درختی",
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json"
  ],
  "files": [
    {
      "path": "registry/kashi/TreeView.tsx",
      "type": "registry:ui",
      "target": "components/kashi/TreeView.tsx",
      "content": "\"use client\";\n\nimport { useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cx } from \"./cx\";\n\nexport interface TreeNode {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n  /** متن کوچک انتهای ردیف، مثل تعداد یا حجم */\n  meta?: ReactNode;\n  disabled?: boolean;\n  children?: TreeNode[];\n}\n\nexport interface TreeViewProps {\n  items: TreeNode[];\n  selected?: string | null;\n  defaultSelected?: string | null;\n  onSelect?: (id: string, node: TreeNode) => void;\n  expanded?: string[];\n  defaultExpanded?: string[];\n  onExpandedChange?: (ids: string[]) => void;\n  \"aria-label\"?: string;\n  className?: string;\n}\n\nconst Chevron = () => <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"m8 5 5 5-5 5\" /></svg>;\n\ninterface Flat { node: TreeNode; level: number; parent: string | null }\n\n/**\n * درخت پوشه‌ها یا دسته‌بندی‌ها با الگوی Tree در WAI-ARIA:\n * ↑↓ حرکت، → و ← (در RTL برعکس) باز و بسته کردن، Home/End، Enter یا Space انتخاب.\n */\nexport function TreeView({ items, selected, defaultSelected = null, onSelect, expanded, defaultExpanded = [], onExpandedChange, \"aria-label\": ariaLabel, className }: TreeViewProps) {\n  const [innerSel, setInnerSel] = useState(defaultSelected);\n  const [innerExp, setInnerExp] = useState(defaultExpanded);\n  const sel = selected !== undefined ? selected : innerSel;\n  const exp = expanded ?? innerExp;\n  const [focusId, setFocusId] = useState<string | null>(null);\n  const root = useRef<HTMLUListElement>(null);\n\n  // ردیف‌های قابل‌مشاهده به ترتیب، برای پیمایش با کیبورد\n  const visible: Flat[] = [];\n  const walk = (nodes: TreeNode[], level: number, parent: string | null) => {\n    for (const n of nodes) {\n      visible.push({ node: n, level, parent });\n      if (n.children?.length && exp.includes(n.id)) walk(n.children, level + 1, n.id);\n    }\n  };\n  walk(items, 1, null);\n  const current = focusId && visible.some((v) => v.node.id === focusId) ? focusId : sel && visible.some((v) => v.node.id === sel) ? sel : visible[0]?.node.id;\n\n  const setExp = (ids: string[]) => {\n    if (expanded === undefined) setInnerExp(ids);\n    onExpandedChange?.(ids);\n  };\n  const toggle = (id: string, open = !exp.includes(id)) => setExp(open ? [...new Set([...exp, id])] : exp.filter((x) => x !== id));\n  const choose = (n: TreeNode) => {\n    if (n.disabled) return;\n    if (selected === undefined) setInnerSel(n.id);\n    onSelect?.(n.id, n);\n  };\n  const focus = (id: string) => {\n    setFocusId(id);\n    root.current?.querySelector<HTMLElement>(`[data-id=\"${CSS.escape(id)}\"]`)?.focus();\n  };\n\n  const onKeyDown = (e: KeyboardEvent) => {\n    const i = visible.findIndex((v) => v.node.id === current);\n    const v = visible[i];\n    if (!v) return;\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n    const inKey = rtl ? \"ArrowLeft\" : \"ArrowRight\";\n    const outKey = rtl ? \"ArrowRight\" : \"ArrowLeft\";\n    const hasKids = !!v.node.children?.length;\n    const open = exp.includes(v.node.id);\n    let handled = true;\n    if (e.key === \"ArrowDown\") visible[i + 1] && focus(visible[i + 1].node.id);\n    else if (e.key === \"ArrowUp\") visible[i - 1] && focus(visible[i - 1].node.id);\n    else if (e.key === \"Home\") focus(visible[0].node.id);\n    else if (e.key === \"End\") focus(visible[visible.length - 1].node.id);\n    else if (e.key === inKey) {\n      if (hasKids && !open) toggle(v.node.id, true);\n      else if (hasKids) focus(v.node.children![0].id);\n    } else if (e.key === outKey) {\n      if (hasKids && open) toggle(v.node.id, false);\n      else if (v.parent) focus(v.parent);\n    } else if (e.key === \"Enter\" || e.key === \" \") choose(v.node);\n    else handled = false;\n    if (handled) e.preventDefault();\n  };\n\n  const render = (nodes: TreeNode[], level: number): ReactNode =>\n    nodes.map((n) => {\n      const hasKids = !!n.children?.length;\n      const open = exp.includes(n.id);\n      return (\n        <li\n          key={n.id}\n          role=\"treeitem\"\n          data-id={n.id}\n          className=\"k-tree__item\"\n          aria-level={level}\n          aria-expanded={hasKids ? open : undefined}\n          aria-selected={sel === n.id}\n          aria-disabled={n.disabled || undefined}\n          tabIndex={current === n.id ? 0 : -1}\n          onFocus={(e) => {\n            if (e.target === e.currentTarget) setFocusId(n.id);\n          }}\n        >\n          <div\n            className=\"k-tree__row\"\n            onClick={() => {\n              if (hasKids) toggle(n.id);\n              choose(n);\n              focus(n.id);\n            }}\n          >\n            {hasKids ? <span className=\"k-tree__toggle\" aria-hidden=\"true\"><Chevron /></span> : <span className=\"k-tree__spacer\" />}\n            {n.icon && <span className=\"k-tree__icon\" aria-hidden=\"true\">{n.icon}</span>}\n            <span className=\"k-tree__label\">{n.label}</span>\n            {n.meta && <span className=\"k-tree__meta\">{n.meta}</span>}\n          </div>\n          {hasKids && open && <ul role=\"group\">{render(n.children!, level + 1)}</ul>}\n        </li>\n      );\n    });\n\n  return (\n    <ul ref={root} role=\"tree\" aria-label={ariaLabel} className={cx(\"k-tree\", className)} onKeyDown={onKeyDown}>\n      {render(items, 1)}\n    </ul>\n  );\n}\n"
    }
  ]
}