{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "checkbox-tree", "type": "registry:ui", "title": "Checkbox Tree", "description": "A hierarchical tree checkbox for nested selections with expand/collapse.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/checkbox-tree.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { motion, AnimatePresence } from \"motion/react\";\n\ninterface TreeNode {\n id: string;\n label: string;\n children?: TreeNode[];\n}\n\ninterface CheckboxTreeProps {\n nodes: TreeNode[];\n value?: string[];\n defaultValue?: string[];\n onValueChange?: (value: string[]) => void;\n className?: string;\n}\n\nfunction getAllChildIds(node: TreeNode): string[] {\n const ids = [node.id];\n if (node.children) {\n node.children.forEach((child) => {\n ids.push(...getAllChildIds(child));\n });\n }\n return ids;\n}\n\nfunction getNodeState(\n node: TreeNode,\n selectedIds: string[],\n): \"checked\" | \"indeterminate\" | \"unchecked\" {\n if (!node.children || node.children.length === 0) {\n return selectedIds.includes(node.id) ? \"checked\" : \"unchecked\";\n }\n\n const childIds = node.children.flatMap((child) => getAllChildIds(child));\n const selectedChildCount = childIds.filter((id) =>\n selectedIds.includes(id),\n ).length;\n\n if (selectedChildCount === 0) return \"unchecked\";\n if (selectedChildCount === childIds.length) return \"checked\";\n return \"indeterminate\";\n}\n\ninterface TreeNodeItemProps {\n node: TreeNode;\n level: number;\n selectedIds: string[];\n onToggle: (node: TreeNode, checked: boolean) => void;\n}\n\nfunction TreeNodeItem({\n node,\n level,\n selectedIds,\n onToggle,\n}: TreeNodeItemProps) {\n const [isExpanded, setIsExpanded] = React.useState(true);\n const hasChildren = node.children && node.children.length > 0;\n const state = getNodeState(node, selectedIds);\n const isChecked = state === \"checked\";\n const isIndeterminate = state === \"indeterminate\";\n const isFilled = isChecked || isIndeterminate;\n\n const handleChange = () => {\n onToggle(node, state !== \"checked\");\n };\n\n return (\n
\n \n {hasChildren ? (\n setIsExpanded(!isExpanded)}\n className=\"flex h-5 w-5 items-center justify-center rounded-[4px] text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600 dark:text-neutral-600 dark:hover:bg-neutral-800 dark:hover:text-neutral-400\"\n >\n \n \n \n \n ) : (\n
\n )}\n \n
\n \n {hasChildren && isExpanded && (\n \n {node.children!.map((child) => (\n \n ))}\n \n )}\n \n
\n );\n}\n\nexport function CheckboxTree({\n nodes,\n value,\n defaultValue = [],\n onValueChange,\n className,\n}: CheckboxTreeProps) {\n const [internalValue, setInternalValue] =\n React.useState(defaultValue);\n const isControlled = value !== undefined;\n const selectedIds = isControlled ? value : internalValue;\n\n const handleToggle = (node: TreeNode, checked: boolean) => {\n const affectedIds = getAllChildIds(node);\n let newValue: string[];\n\n if (checked) {\n newValue = [...new Set([...selectedIds, ...affectedIds])];\n } else {\n newValue = selectedIds.filter((id) => !affectedIds.includes(id));\n }\n\n if (!isControlled) {\n setInternalValue(newValue);\n }\n onValueChange?.(newValue);\n };\n\n return (\n
\n {nodes.map((node) => (\n \n ))}\n
\n );\n}\n", "type": "registry:ui", "target": "components/ruixen/checkbox-tree.tsx" } ] }