{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "BranchedMenu-TS-CSS",
	"title": "BranchedMenu",
	"description": "Collapsible menu whose sections unfold into a trunk with a curved branch to each child, and an accent line that travels down the trunk and around the curve to whatever you pick, while a marker glides to the open section.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "BranchedMenu.css",
			"target": "@components/BranchedMenu.css",
			"content": ".branched-menu {\n  --bm-w: 240px;\n  --bm-ink: #f5f5f5;\n  --bm-accent: #f5f5f5;\n  --bm-line: #3f3f46;\n  --bm-font: 14px;\n  --bm-row: 36px;\n  --bm-indent: 40px;\n  --bm-line-w: 1.5;\n  --bm-draw: 400ms;\n  --bm-fold: 300ms;\n  --bm-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n  --bm-muted: color-mix(in srgb, var(--bm-ink) 55%, transparent);\n\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: fit-content;\n  max-width: min(var(--bm-w), 100%);\n  padding-left: 14px;\n  color: var(--bm-ink);\n  font-family: inherit;\n  font-size: var(--bm-font);\n  line-height: 1.2;\n}\n\n.branched-menu::before {\n  content: '';\n  position: absolute;\n  top: 8px;\n  bottom: 0;\n  left: 0;\n  width: 2px;\n  border-radius: 1px;\n  background: linear-gradient(to bottom, var(--bm-line) 0%, var(--bm-line) 55%, transparent 100%);\n}\n\n.branched-menu__marker {\n  position: absolute;\n  top: -1px;\n  left: 0;\n  z-index: 1;\n  width: 2px;\n  height: 16px;\n  border-radius: 1px;\n  background: var(--bm-accent);\n  opacity: 0;\n  transition:\n    top 220ms var(--bm-ease-out),\n    opacity 150ms ease;\n}\n\n.branched-menu__marker[data-on] {\n  opacity: 1;\n}\n\n.branched-menu__section {\n  display: flex;\n  flex-direction: column;\n}\n\n.branched-menu__head {\n  display: block;\n  margin: 0;\n  padding: 9px 0;\n  border: 0;\n  background: none;\n  color: var(--bm-muted);\n  font: inherit;\n  font-size: calc(var(--bm-font) + 1px);\n  font-weight: 500;\n  text-align: left;\n  cursor: pointer;\n  outline: none;\n  -webkit-tap-highlight-color: transparent;\n  transition: color 200ms ease;\n}\n\n.branched-menu__section[data-open] .branched-menu__head,\n.branched-menu__head[data-active] {\n  color: var(--bm-ink);\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .branched-menu__head:hover {\n    color: var(--bm-ink);\n  }\n}\n\n.branched-menu__body {\n  display: grid;\n  grid-template-rows: 0fr;\n  transition: grid-template-rows var(--bm-fold) var(--bm-ease-out);\n}\n\n.branched-menu__section[data-open] .branched-menu__body {\n  grid-template-rows: 1fr;\n}\n\n.branched-menu__fold {\n  min-height: 0;\n  overflow: hidden;\n}\n\n.branched-menu__tree {\n  position: relative;\n  padding: 6px 0;\n  box-sizing: border-box;\n}\n\n.branched-menu__lines {\n  position: absolute;\n  top: 0;\n  left: 0;\n  overflow: visible;\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 200ms ease;\n}\n\n.branched-menu__section[data-open] .branched-menu__lines {\n  opacity: 1;\n  transition: opacity 250ms ease 100ms;\n}\n\n.branched-menu__base,\n.branched-menu__reach {\n  fill: none;\n  stroke-width: var(--bm-line-w);\n  stroke-linecap: round;\n  stroke-linejoin: round;\n}\n\n.branched-menu__base {\n  stroke: var(--bm-line);\n}\n\n.branched-menu__reach {\n  stroke: var(--bm-accent);\n  transition: stroke-dashoffset var(--bm-draw) var(--bm-ease-out);\n}\n\n.branched-menu__item {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  width: 100%;\n  height: var(--bm-row);\n  margin: 0;\n  padding: 0 0 0 var(--bm-indent);\n  border: 0;\n  background: none;\n  color: var(--bm-muted);\n  font: inherit;\n  text-align: left;\n  cursor: pointer;\n  outline: none;\n  box-sizing: border-box;\n  -webkit-tap-highlight-color: transparent;\n  transition: color 200ms ease;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .branched-menu__item:hover {\n    color: var(--bm-ink);\n  }\n}\n\n.branched-menu__item[data-active] {\n  color: var(--bm-accent);\n  font-weight: 500;\n}\n\n.branched-menu__icon {\n  display: inline-flex;\n  flex: none;\n}\n\n.branched-menu__label {\n  white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .branched-menu__marker {\n    transition: opacity 150ms ease;\n  }\n\n  .branched-menu__body {\n    transition: none;\n  }\n\n  .branched-menu__reach {\n    transition: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "BranchedMenu.tsx",
			"content": "'use client';\n\nimport React, { isValidElement, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';\nimport { HugeiconsIcon, type IconSvgElement } from '@hugeicons/react';\nimport {\n  CursorPointer01Icon,\n  Download04Icon,\n  Layers01Icon,\n  Notification03Icon,\n  PaintBoardIcon,\n  Rocket01Icon,\n  Settings02Icon,\n  TextFontIcon\n} from '@hugeicons/core-free-icons';\nimport './BranchedMenu.css';\n\nexport interface BranchedMenuChild {\n  value: string;\n  label: string;\n  icon?: ReactNode | IconSvgElement;\n}\n\nexport interface BranchedMenuItem {\n  label: string;\n  value?: string;\n  children?: BranchedMenuChild[];\n}\n\nexport interface BranchedMenuProps {\n  items?: BranchedMenuItem[];\n  defaultOpen?: number | number[];\n  defaultActive?: string;\n  onSelect?: (value: string, item: BranchedMenuChild | BranchedMenuItem) => void;\n  onToggle?: (index: number, open: boolean) => void;\n  color?: string;\n  accentColor?: string;\n  lineColor?: string;\n  width?: number;\n  rowHeight?: number;\n  indent?: number;\n  trunk?: number;\n  radius?: number;\n  lineWidth?: number;\n  fontSize?: number;\n  drawDuration?: number;\n  foldDuration?: number;\n  className?: string;\n}\n\nconst DEFAULT_ITEMS: BranchedMenuItem[] = [\n  {\n    label: 'Getting started',\n    children: [\n      { value: 'install', label: 'Installation', icon: Download04Icon },\n      { value: 'quick', label: 'Quick start', icon: Rocket01Icon },\n      { value: 'config', label: 'Configuration', icon: Settings02Icon },\n      { value: 'theming', label: 'Theming', icon: PaintBoardIcon }\n    ]\n  },\n  {\n    label: 'Components',\n    children: [\n      { value: 'buttons', label: 'Buttons', icon: CursorPointer01Icon },\n      { value: 'typography', label: 'Typography', icon: TextFontIcon },\n      { value: 'overlays', label: 'Overlays', icon: Layers01Icon },\n      { value: 'toasts', label: 'Toasts', icon: Notification03Icon }\n    ]\n  }\n];\nconst PAD = 6;\nconst MARK = 16;\n\nconst renderIcon = (icon: ReactNode | IconSvgElement) =>\n  isValidElement(icon) ? icon : <HugeiconsIcon icon={icon as IconSvgElement} size={16} strokeWidth={1.8} />;\nconst toSet = (open: number | number[]) => new Set(Array.isArray(open) ? open : open >= 0 ? [open] : []);\n\nconst BranchedMenu: React.FC<BranchedMenuProps> = ({\n  items = DEFAULT_ITEMS,\n  defaultOpen = 0,\n  defaultActive = '',\n  onSelect,\n  onToggle,\n  color = '#f5f5f5',\n  accentColor = '#f5f5f5',\n  lineColor = '#3f3f46',\n  width = 240,\n  rowHeight = 36,\n  indent = 40,\n  trunk = 14,\n  radius = 10,\n  lineWidth = 1.5,\n  fontSize = 14,\n  drawDuration = 400,\n  foldDuration = 300,\n  className = ''\n}) => {\n  const [open, setOpen] = useState<Set<number>>(() => toSet(defaultOpen));\n  const [active, setActive] = useState(() => {\n    if (defaultActive) return defaultActive;\n    const first = items.find((it, i) => it.children && toSet(defaultOpen).has(i));\n    return first?.children?.[0]?.value ?? '';\n  });\n  const navRef = useRef<HTMLElement>(null);\n  const heads = useRef<(HTMLButtonElement | null)[]>([]);\n  const markerRef = useRef<HTMLSpanElement>(null);\n  const latest = useRef<{ onSelect?: BranchedMenuProps['onSelect']; onToggle?: BranchedMenuProps['onToggle'] }>({});\n  latest.current = { onSelect, onToggle };\n\n  const activeSection = items.findIndex(it => it.children?.some(kid => kid.value === active));\n  const markerShown = activeSection >= 0 && open.has(activeSection);\n  useLayoutEffect(() => {\n    const place = (glide: boolean) => {\n      const m = markerRef.current;\n      const el = heads.current[activeSection];\n      if (!m) return;\n      const on = markerShown && el;\n      if (!glide) m.style.transition = 'none';\n      if (on) m.style.top = `${el.offsetTop + (el.offsetHeight - MARK) / 2}px`;\n      m.toggleAttribute('data-on', Boolean(on));\n      if (!glide) {\n        void m.offsetHeight;\n        m.style.transition = '';\n      }\n    };\n    place(true);\n    let first = true;\n    const ro = new ResizeObserver(() => {\n      if (first) {\n        first = false;\n        return;\n      }\n      place(false);\n    });\n    if (navRef.current) ro.observe(navRef.current);\n    return () => ro.disconnect();\n  }, [activeSection, markerShown, items, fontSize, rowHeight]);\n\n  const select = (value: string, item: BranchedMenuChild | BranchedMenuItem) => {\n    setActive(value);\n    latest.current.onSelect?.(value, item);\n  };\n  const toggle = (i: number) => {\n    setOpen(prev => {\n      const next = new Set(prev);\n      const isOpen = !next.has(i);\n      if (isOpen) next.add(i);\n      else next.delete(i);\n      latest.current.onToggle?.(i, isOpen);\n      return next;\n    });\n  };\n\n  const r = Math.min(radius, rowHeight / 2 - 2);\n  const endX = indent - 8;\n  const rowY = (k: number) => PAD + k * rowHeight + rowHeight / 2;\n  const branch = (k: number) => `M ${trunk} ${rowY(k) - r} A ${r} ${r} 0 0 0 ${trunk + r} ${rowY(k)} H ${endX}`;\n  const reach = (k: number) => `M ${trunk} 0 V ${rowY(k) - r} A ${r} ${r} 0 0 0 ${trunk + r} ${rowY(k)} H ${endX}`;\n  const length = (k: number) => rowY(k) - r + (Math.PI * r) / 2 + (endX - trunk - r);\n\n  return (\n    <nav\n      ref={navRef}\n      className={`branched-menu${className ? ` ${className}` : ''}`}\n      style={\n        {\n          '--bm-w': `${width}px`,\n          '--bm-ink': color,\n          '--bm-accent': accentColor,\n          '--bm-line': lineColor,\n          '--bm-font': `${fontSize}px`,\n          '--bm-row': `${rowHeight}px`,\n          '--bm-indent': `${indent}px`,\n          '--bm-line-w': lineWidth,\n          '--bm-draw': `${drawDuration}ms`,\n          '--bm-fold': `${foldDuration}ms`\n        } as CSSProperties\n      }\n    >\n      <span ref={markerRef} className=\"branched-menu__marker\" aria-hidden=\"true\" />\n      {items.map((item, i) => {\n        const kids = item.children;\n        const isOpen = kids ? open.has(i) : false;\n        const leafValue = item.value ?? item.label;\n        const leafActive = !kids && leafValue === active;\n        const bodyH = kids ? PAD * 2 + kids.length * rowHeight : 0;\n        return (\n          <div key={item.value ?? item.label} className=\"branched-menu__section\" data-open={isOpen ? '' : undefined}>\n            <button\n              ref={el => {\n                heads.current[i] = el;\n              }}\n              type=\"button\"\n              className=\"branched-menu__head\"\n              aria-expanded={kids ? isOpen : undefined}\n              aria-current={leafActive ? 'true' : undefined}\n              data-active={leafActive ? '' : undefined}\n              onClick={() => (kids ? toggle(i) : select(leafValue, item))}\n            >\n              {item.label}\n            </button>\n            {kids ? (\n              <div className=\"branched-menu__body\">\n                <div className=\"branched-menu__fold\">\n                  <div className=\"branched-menu__tree\" style={{ height: bodyH }}>\n                    <svg className=\"branched-menu__lines\" width={indent} height={bodyH} aria-hidden=\"true\">\n                      <path className=\"branched-menu__base\" d={`M ${trunk} 0 V ${rowY(kids.length - 1) - r}`} />\n                      {kids.map((kid, k) => (\n                        <path key={kid.value} className=\"branched-menu__base\" d={branch(k)} />\n                      ))}\n                      {kids.map((kid, k) => (\n                        <path\n                          key={kid.value}\n                          className=\"branched-menu__reach\"\n                          d={reach(k)}\n                          style={{\n                            strokeDasharray: length(k),\n                            strokeDashoffset: kid.value === active ? 0 : length(k)\n                          }}\n                        />\n                      ))}\n                    </svg>\n                    {kids.map(kid => (\n                      <button\n                        key={kid.value}\n                        type=\"button\"\n                        className=\"branched-menu__item\"\n                        aria-current={kid.value === active ? 'true' : undefined}\n                        data-active={kid.value === active ? '' : undefined}\n                        tabIndex={isOpen ? 0 : -1}\n                        onClick={() => select(kid.value, kid)}\n                      >\n                        {kid.icon ? (\n                          <span className=\"branched-menu__icon\" aria-hidden=\"true\">\n                            {renderIcon(kid.icon)}\n                          </span>\n                        ) : null}\n                        <span className=\"branched-menu__label\">{kid.label}</span>\n                      </button>\n                    ))}\n                  </div>\n                </div>\n              </div>\n            ) : null}\n          </div>\n        );\n      })}\n    </nav>\n  );\n};\n\nexport default BranchedMenu;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}