{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "tree-view",
  "title": "Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/tree-view/TreeView.vue",
      "content": "<script setup lang=\"ts\">\nimport { onMounted, provide, ref, toRef, watch, type HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { TREE_VIEW_CONTEXT } from './context'\nimport type { TreeViewItem } from './types'\n\ninterface Props {\n  items: TreeViewItem[]\n  class?: HTMLAttributes['class']\n  showIcons?: boolean\n  showCheckboxes?: boolean\n  defaultExpanded?: boolean\n  selectedId?: string | null\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  showIcons: true,\n  showCheckboxes: false,\n  defaultExpanded: false,\n  selectedId: null,\n})\n\nconst emit = defineEmits<{\n  select: [item: TreeViewItem]\n  toggle: [item: TreeViewItem]\n  'update:selectedId': [id: string | null]\n}>()\n\nconst expandedIds = ref<Set<string>>(new Set())\nconst selectedId = ref<string | null>(props.selectedId)\n\nwatch(\n  () => props.selectedId,\n  (val) => {\n    selectedId.value = val\n  },\n)\n\nfunction toggle(item: TreeViewItem) {\n  if (item.disabled) return\n  const next = new Set(expandedIds.value)\n  if (next.has(item.id)) next.delete(item.id)\n  else next.add(item.id)\n  expandedIds.value = next\n  emit('toggle', item)\n}\n\nfunction select(item: TreeViewItem) {\n  if (item.disabled) return\n  selectedId.value = item.id\n  emit('select', item)\n  emit('update:selectedId', item.id)\n}\n\nprovide(TREE_VIEW_CONTEXT, {\n  expandedIds,\n  selectedId,\n  showIcons: toRef(props, 'showIcons'),\n  showCheckboxes: toRef(props, 'showCheckboxes'),\n  toggle,\n  select,\n})\n\nonMounted(() => {\n  if (!props.defaultExpanded) return\n  const next = new Set<string>()\n  const walk = (items: TreeViewItem[]) => {\n    for (const it of items) {\n      if (it.children?.length) {\n        next.add(it.id)\n        walk(it.children)\n      }\n    }\n  }\n  walk(props.items)\n  expandedIds.value = next\n})\n</script>\n\n<template>\n  <div :class=\"cn('text-sm', props.class)\" role=\"tree\">\n    <TreeViewNode v-for=\"(item, i) in items\" :key=\"item.id\" :item=\"item\" :depth=\"0\" :is-last=\"i === items.length - 1\" />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/tree-view/TreeView.vue"
    },
    {
      "path": "packages/registry-vue/components/tree-view/TreeViewNode.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, inject } from 'vue'\nimport { ChevronRight, File, Folder, FolderOpen } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { TREE_VIEW_CONTEXT } from './context'\nimport type { TreeViewItem } from './types'\n\ninterface Props {\n  item: TreeViewItem\n  depth: number\n  isLast?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  isLast: false,\n})\n\nconst _maybeCtx = inject(TREE_VIEW_CONTEXT)\nif (!_maybeCtx) throw new Error('TreeViewNode must be used inside <TreeView>')\nconst ctx: NonNullable<typeof _maybeCtx> = _maybeCtx\n\nconst isExpanded = computed(() => ctx.expandedIds.value.has(props.item.id))\nconst isSelected = computed(() => ctx.selectedId.value === props.item.id)\nconst hasChildren = computed(() => !!(props.item.children && props.item.children.length))\n\nconst Icon = computed(() => {\n  if (!ctx.showIcons.value) return null\n  if (props.item.icon) return props.item.icon\n  if (!hasChildren.value) return File\n  return isExpanded.value ? FolderOpen : Folder\n})\n\nfunction handleToggle(e: Event) {\n  e.stopPropagation()\n  ctx.toggle(props.item)\n}\n\nfunction handleSelect() {\n  if (props.item.disabled) return\n  ctx.select(props.item)\n}\n\n// 20px per level. Connector lives in parent's gutter (depth - 1).\nconst INDENT = 20\nconst rowPadLeft = computed(() => `${props.depth * INDENT + 4}px`)\nconst connectorLeft = computed(() => `${(props.depth - 1) * INDENT + 10}px`)\n</script>\n\n<template>\n  <div role=\"treeitem\" :aria-expanded=\"hasChildren ? isExpanded : undefined\" class=\"relative\">\n    <!-- Discord-style elbow + trunk for non-root nodes. The elbow points\n         from the parent's chevron column down to this row's center; the\n         trunk continues to the next sibling at this depth (omitted on the\n         last sibling). -->\n    <span\n      v-if=\"depth > 0\"\n      aria-hidden=\"true\"\n      class=\"border-border pointer-events-none absolute top-0 h-4 w-3 rounded-bl-md border-b border-l\"\n      :style=\"{ left: connectorLeft }\"\n    />\n    <span\n      v-if=\"depth > 0 && !isLast\"\n      aria-hidden=\"true\"\n      class=\"bg-border pointer-events-none absolute top-4 bottom-0 w-px\"\n      :style=\"{ left: connectorLeft }\"\n    />\n\n    <!-- Row -->\n    <div\n      :class=\"\n        cn(\n          'group relative flex h-8 cursor-pointer items-center gap-1.5 rounded-md pr-2 text-sm transition-colors',\n          'hover:bg-accent hover:text-accent-foreground',\n          'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none',\n          item.disabled && 'cursor-not-allowed opacity-50',\n          isSelected && 'bg-accent text-accent-foreground font-medium',\n        )\n      \"\n      :style=\"{ paddingLeft: rowPadLeft }\"\n      :tabindex=\"item.disabled ? -1 : 0\"\n      role=\"button\"\n      @click=\"handleSelect\"\n      @keydown.enter.prevent=\"handleSelect\"\n      @keydown.space.prevent=\"handleSelect\"\n    >\n      <!-- Chevron (or 16px spacer for leaves so labels align) -->\n      <button\n        v-if=\"hasChildren\"\n        type=\"button\"\n        :class=\"\n          cn(\n            'focus-visible:ring-ring flex size-4 shrink-0 items-center justify-center rounded transition-transform duration-150 focus-visible:ring-2 focus-visible:outline-none',\n            'hover:bg-foreground/10',\n            isExpanded && 'rotate-90',\n          )\n        \"\n        :aria-label=\"isExpanded ? 'Collapse' : 'Expand'\"\n        @click=\"handleToggle\"\n      >\n        <ChevronRight class=\"text-muted-foreground size-3.5\" aria-hidden=\"true\" />\n      </button>\n      <span v-else class=\"size-4 shrink-0\" />\n\n      <!-- Checkbox -->\n      <input\n        v-if=\"ctx.showCheckboxes.value\"\n        type=\"checkbox\"\n        :checked=\"item.selected\"\n        :disabled=\"item.disabled\"\n        class=\"border-input bg-background text-primary focus:ring-ring focus-visible:ring-ring size-3.5 shrink-0 rounded focus:ring-1 focus-visible:ring-2 focus-visible:outline-none\"\n        @change=\"handleSelect\"\n        @click.stop\n      />\n\n      <!-- Icon -->\n      <component\n        :is=\"Icon\"\n        v-if=\"Icon\"\n        :class=\"cn('size-4 shrink-0', hasChildren ? 'text-primary' : 'text-muted-foreground')\"\n      />\n\n      <!-- Label -->\n      <span class=\"flex-1 truncate\">{{ item.label }}</span>\n    </div>\n\n    <!-- Children -->\n    <div v-if=\"hasChildren && isExpanded\" role=\"group\">\n      <TreeViewNode\n        v-for=\"(child, j) in item.children\"\n        :key=\"child.id\"\n        :item=\"child\"\n        :depth=\"depth + 1\"\n        :is-last=\"j === (item.children?.length ?? 0) - 1\"\n      />\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/tree-view/TreeViewNode.vue"
    },
    {
      "path": "packages/registry-vue/components/tree-view/context.ts",
      "content": "import type { InjectionKey, Ref } from 'vue'\nimport type { TreeViewItem } from './types'\n\nexport interface TreeViewContext {\n  expandedIds: Ref<Set<string>>\n  selectedId: Ref<string | null>\n  showIcons: Ref<boolean>\n  showCheckboxes: Ref<boolean>\n  toggle: (item: TreeViewItem) => void\n  select: (item: TreeViewItem) => void\n}\n\nexport const TREE_VIEW_CONTEXT: InjectionKey<TreeViewContext> = Symbol('TreeViewContext')\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/tree-view/context.ts"
    },
    {
      "path": "packages/registry-vue/components/tree-view/types.ts",
      "content": "import type { File } from 'lucide-vue-next'\n\nexport interface TreeViewItem {\n  id: string\n  label: string\n  icon?: typeof File\n  children?: TreeViewItem[]\n  disabled?: boolean\n  selected?: boolean\n  expanded?: boolean\n  [key: string]: unknown\n}\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/tree-view/types.ts"
    },
    {
      "path": "packages/registry-vue/components/tree-view/index.ts",
      "content": "export { default as TreeView } from './TreeView.vue'\nexport { default as TreeViewNode } from './TreeViewNode.vue'\nexport type { TreeViewItem } from './types'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/tree-view/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Indented tree of expandable nodes — file browsers, taxonomy editors, nested settings. Discord-style elbow connectors, lazy-load branches, and full keyboard navigation.",
  "categories": [
    "data-display"
  ]
}