{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tree", "type": "registry:component", "registryDependencies": [], "files": [ { "path": "components/domain-ui/tree.tsx", "content": "\"use client\";\nimport { ChevronRight, File, Folder, FolderOpen } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport {\n type ComponentProps,\n createContext,\n type HTMLAttributes,\n type ReactNode,\n useCallback,\n useContext,\n useId,\n useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype TreeContextType = {\n expandedIds: Set;\n selectedIds: string[];\n toggleExpanded: (nodeId: string) => void;\n handleSelection: (nodeId: string, ctrlKey: boolean) => void;\n showLines?: boolean;\n showIcons?: boolean;\n selectable?: boolean;\n multiSelect?: boolean;\n indent?: number;\n animateExpand?: boolean;\n};\n\nconst TreeContext = createContext(undefined);\n\nconst useTree = () => {\n const context = useContext(TreeContext);\n if (!context) {\n throw new Error(\"Tree components must be used within a TreeProvider\");\n }\n return context;\n};\n\ntype TreeNodeContextType = {\n nodeId: string;\n level: number;\n isLast: boolean;\n parentPath: boolean[];\n};\n\nconst TreeNodeContext = createContext(\n undefined\n);\n\nconst useTreeNode = () => {\n const context = useContext(TreeNodeContext);\n if (!context) {\n throw new Error(\"TreeNode components must be used within a TreeNode\");\n }\n return context;\n};\n\nexport type TreeProviderProps = {\n children: ReactNode;\n defaultExpandedIds?: string[];\n showLines?: boolean;\n showIcons?: boolean;\n selectable?: boolean;\n multiSelect?: boolean;\n selectedIds?: string[];\n onSelectionChange?: (selectedIds: string[]) => void;\n indent?: number;\n animateExpand?: boolean;\n className?: string;\n};\n\nexport const TreeProvider = ({\n children,\n defaultExpandedIds = [],\n showLines = true,\n showIcons = true,\n selectable = true,\n multiSelect = false,\n selectedIds,\n onSelectionChange,\n indent = 19,\n animateExpand = true,\n className,\n}: TreeProviderProps) => {\n const [expandedIds, setExpandedIds] = useState>(\n new Set(defaultExpandedIds)\n );\n const [internalSelectedIds, setInternalSelectedIds] = useState(\n selectedIds ?? []\n );\n\n const isControlled =\n selectedIds !== undefined && onSelectionChange !== undefined;\n const currentSelectedIds = isControlled ? selectedIds : internalSelectedIds;\n\n const toggleExpanded = useCallback((nodeId: string) => {\n setExpandedIds((prev) => {\n const newSet = new Set(prev);\n if (newSet.has(nodeId)) {\n newSet.delete(nodeId);\n } else {\n newSet.add(nodeId);\n }\n return newSet;\n });\n }, []);\n\n const handleSelection = useCallback(\n (nodeId: string, ctrlKey = false) => {\n if (!selectable) {\n return;\n }\n\n let newSelection: string[];\n\n if (multiSelect && ctrlKey) {\n newSelection = currentSelectedIds.includes(nodeId)\n ? currentSelectedIds.filter((id) => id !== nodeId)\n : [...currentSelectedIds, nodeId];\n } else {\n newSelection = currentSelectedIds.includes(nodeId) ? [] : [nodeId];\n }\n\n if (isControlled) {\n onSelectionChange?.(newSelection);\n } else {\n setInternalSelectedIds(newSelection);\n }\n },\n [\n selectable,\n multiSelect,\n currentSelectedIds,\n isControlled,\n onSelectionChange,\n ]\n );\n\n return (\n \n \n {children}\n \n \n );\n};\n\nexport type TreeViewProps = HTMLAttributes;\n\nexport const TreeView = ({ className, children, ...props }: TreeViewProps) => (\n
\n {children}\n
\n);\n\nexport type TreeNodeProps = HTMLAttributes & {\n nodeId?: string;\n level?: number;\n isLast?: boolean;\n parentPath?: boolean[];\n children?: ReactNode;\n};\n\nexport const TreeNode = ({\n nodeId: providedNodeId,\n level = 0,\n isLast = false,\n parentPath = [],\n children,\n className,\n onClick,\n ...props\n}: TreeNodeProps) => {\n const generatedId = useId();\n const nodeId = providedNodeId ?? generatedId;\n\n // Build the parent path - mark positions where the parent was the last child\n const currentPath = level === 0 ? [] : [...parentPath];\n if (level > 0 && parentPath.length < level - 1) {\n // Fill in missing levels with false (not last)\n while (currentPath.length < level - 1) {\n currentPath.push(false);\n }\n }\n if (level > 0) {\n currentPath[level - 1] = isLast;\n }\n\n return (\n \n
\n {children}\n
\n \n );\n};\n\nexport type TreeNodeTriggerProps = ComponentProps;\n\nexport const TreeNodeTrigger = ({\n children,\n className,\n onClick,\n ...props\n}: TreeNodeTriggerProps) => {\n const { selectedIds, toggleExpanded, handleSelection, expandedIds } =\n useTree();\n const { nodeId, level } = useTreeNode();\n const isSelected = selectedIds.includes(nodeId);\n const isExpanded = expandedIds.has(nodeId);\n\n return (\n {\n toggleExpanded(nodeId);\n handleSelection(nodeId, e.ctrlKey || e.metaKey);\n onClick?.(e);\n }}\n style={{\n paddingLeft: 24 + ((level - 1) * 38) / 2,\n }}\n whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}\n {...props}\n >\n \n {children as ReactNode}\n \n );\n};\n\nexport const TreeLines = () => {\n const { showLines, selectedIds } = useTree();\n const { level, nodeId } = useTreeNode();\n const isSelected = selectedIds.includes(nodeId);\n\n const CHEVRON_ICON_SIZE = 14;\n\n return (\n <>\n {showLines &&\n Array.from({ length: level - 1 }).map((_, i) => (\n \n ))}\n {isSelected && (\n
\n )}\n \n );\n};\n\nexport type TreeNodeContentProps = ComponentProps & {\n hasChildren?: boolean;\n};\n\nexport const TreeNodeContent = ({\n children,\n hasChildren = false,\n className,\n ...props\n}: TreeNodeContentProps) => {\n const { animateExpand, expandedIds } = useTree();\n const { nodeId } = useTreeNode();\n const isExpanded = expandedIds.has(nodeId);\n\n return (\n \n {hasChildren && isExpanded && (\n \n \n {children}\n \n \n )}\n \n );\n};\n\nexport type TreeExpanderProps = ComponentProps & {\n hasChildren?: boolean;\n};\n\nexport const TreeExpander = ({\n hasChildren = false,\n className,\n onClick,\n ...props\n}: TreeExpanderProps) => {\n const { expandedIds, toggleExpanded } = useTree();\n const { nodeId } = useTreeNode();\n const isExpanded = expandedIds.has(nodeId);\n\n if (!hasChildren) {\n return null;\n }\n\n return (\n {\n e.stopPropagation();\n toggleExpanded(nodeId);\n onClick?.(e);\n }}\n transition={{ duration: 0.2, ease: \"easeInOut\" }}\n {...props}\n >\n \n \n );\n};\n\nexport type TreeIconProps = ComponentProps & {\n icon?: ReactNode;\n hasChildren?: boolean;\n};\n\nexport const TreeIcon = ({\n icon,\n hasChildren = false,\n className,\n ...props\n}: TreeIconProps) => {\n const { showIcons, expandedIds } = useTree();\n const { nodeId } = useTreeNode();\n const isExpanded = expandedIds.has(nodeId);\n\n if (!showIcons) {\n return null;\n }\n\n const getDefaultIcon = () =>\n hasChildren ? (\n isExpanded ? (\n \n ) : (\n \n )\n ) : (\n icon || (\n \n )\n );\n\n return (\n \n {getDefaultIcon()}\n \n );\n};\n\nexport type TreeLabelProps = HTMLAttributes;\n\nexport const TreeLabel = ({ className, ...props }: TreeLabelProps) => (\n \n);\n", "type": "registry:component" } ], "meta": { "tags": [ "tree", "file-tree", "hierarchy", "navigation", "expand", "collapse", "nested", "folders", "files" ] } }