@behindthemusictree/genre-tree-view 6.2.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/GenreTree.tsx","../src/NodeHelper.tsx","../src/constants.ts","../src/types.ts","../src/d3-helper/d3-grid-helper.ts","../src/d3-helper/d3-path-helper.ts","../src/tree-renderer.ts","../src/FloatingControls.tsx","../src/BusyIndicator.tsx","../src/use-pan-zoom.ts","../src/zoom-pan.ts","../src/use-node-info-panel.ts","../src/info-panel-geometry.ts","../src/use-tree-index.ts","../src/tree-index.ts","../src/InfoPanel.tsx","../src/InfoPanelExpandButton.tsx","../src/InfoPanelSection.tsx","../src/NodeToolbar.tsx","../src/OverlayPanel.tsx","../src/GenreTreeWheelBase.tsx","../src/root-grouping.ts","../src/wheel-geometry.ts","../src/GenreTreeWheel.tsx","../src/GenreTreeWheelRight.tsx","../src/GenreTreeWheelRadialBase.tsx","../src/defer-click.ts","../src/radial-wheel-geometry.ts","../src/pop-core-radial-layout.ts","../src/core-radial-layout.ts","../src/pop-core-split.ts","../src/GenreTreeWheelRadial.tsx","../src/GenreTreeWheelRadialPopCoreBase.tsx","../src/use-focus-history.ts","../src/WheelBreadcrumb.tsx","../src/pop-core-frame.ts","../src/GenreTreeWheelRadialPopCore.tsx","../src/GenreTreeOutline.tsx","../src/GenreTreeSkeleton.tsx","../src/GenreTreeWheelSkeleton.tsx","../src/GenreTreeOutlineSkeleton.tsx","../src/GenreTreeViewSkeleton.tsx"],"sourcesContent":["export { GenreTree } from \"./GenreTree\";\nexport { GenreTreeWheel } from \"./GenreTreeWheel\";\nexport { GenreTreeWheelRight } from \"./GenreTreeWheelRight\";\nexport { GenreTreeWheelRadial } from \"./GenreTreeWheelRadial\";\nexport { GenreTreeWheelRadialPopCore } from \"./GenreTreeWheelRadialPopCore\";\nexport { GenreTreeOutline } from \"./GenreTreeOutline\";\nexport { GenreTreeSkeleton } from \"./GenreTreeSkeleton\";\nexport { GenreTreeWheelSkeleton } from \"./GenreTreeWheelSkeleton\";\nexport { GenreTreeOutlineSkeleton } from \"./GenreTreeOutlineSkeleton\";\nexport { GenreTreeViewSkeleton } from \"./GenreTreeViewSkeleton\";\nexport { getGenreTreeColor, DEFAULT_FRAME_WIDTH, DEFAULT_FRAME_HEIGHT } from \"./constants\";\nexport { groupNodesByRoot } from \"./root-grouping\";\nexport { isZoomableNode } from \"./pop-core-frame\";\nexport type {\n GenreTreeNode,\n GenreTreeProps,\n GenreTreePlayState,\n GenreTreeViewMode,\n TreeOrientation,\n GenreTreeAction,\n WheelZoomMode,\n ControlsOrientation,\n GenreTreeOverlayPanel,\n GenreTreeViewport,\n InfoPanelBody,\n InfoPanelSections,\n} from \"./types\";\nexport type { GenreTreeViewSkeletonProps } from \"./GenreTreeViewSkeleton\";\nexport type { GenreTreeWheelProps } from \"./GenreTreeWheel\";\nexport type { GenreTreeWheelRightProps } from \"./GenreTreeWheelRight\";\nexport type { GenreTreeWheelRadialProps } from \"./GenreTreeWheelRadial\";\nexport type { GenreTreeWheelRadialPopCoreProps } from \"./GenreTreeWheelRadialPopCore\";\nexport type { FocusChangeReason } from \"./GenreTreeWheelRadialPopCoreBase\";\nexport type { GenreTreeOutlineProps } from \"./GenreTreeOutline\";\nexport type { GenreTreeRootGroup } from \"./root-grouping\";\n","\"use client\";\n\nimport { useEffect, useRef, useMemo } from \"react\";\nimport * as d3 from \"d3\";\n\nimport { GenreTreeProps } from \"./types\";\nimport { buildTreeHierarchyStructure } from \"./NodeHelper\";\nimport {\n calculateSvgDimensions,\n createTreeLayout,\n setupTreeLayout,\n renderTree,\n} from \"./tree-renderer\";\nimport {\n ACCENT_TEXT_COLOR,\n getGenreTreeColor,\n INFO_PANEL_WIDTH,\n TEXT_COLOR,\n tintSurface,\n ZOOM_FOCUS_SCALE,\n} from \"./constants\";\nimport { FloatingControls } from \"./FloatingControls\";\nimport { usePanZoom } from \"./use-pan-zoom\";\nimport { queryTreeContentElements } from \"./zoom-pan\";\nimport { useNodeInfoPanel } from \"./use-node-info-panel\";\nimport { useTreeIndex } from \"./use-tree-index\";\nimport { resolveInfoPanelObscuredArea } from \"./info-panel-geometry\";\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\n\n/**\n * Renders one connected hierarchy of `GenreTreeNode`s as an interactive D3/SVG tree.\n * Purely presentational: fetching, mutations, and popups are the consumer's responsibility,\n * wired through the callback props.\n */\nexport function GenreTree({\n nodes,\n className,\n rootColor,\n orientation = \"horizontal\",\n hideRoot = false,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n interactive = true,\n depthSpacingScale = 1,\n showToolbar = true,\n selectedNodeId: selectedNodeIdProp,\n renderInfoPanelBody,\n onSelectedNodeChange,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n wheelZoom,\n defaultViewport,\n onViewportChange,\n controlsOrientation = \"vertical\",\n floatingControlsLeading,\n busy,\n}: GenreTreeProps) {\n const index = useTreeIndex(nodes);\n const svgRef = useRef<SVGSVGElement>(null);\n const viewportRef = useRef<HTMLDivElement>(null);\n const stageRef = useRef<HTMLDivElement>(null);\n const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });\n const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId: selectedNodeIdProp, onSelectedNodeChange });\n const showNodeInfoRef = useRef(showNodeInfo);\n useEffect(() => {\n showNodeInfoRef.current = showNodeInfo;\n });\n // Read via a ref inside the D3 render effect below rather than depending on `panZoom` directly —\n // its identity changes every render, which would otherwise re-run (and re-mount the whole D3\n // tree) on every pan/zoom, exactly what that effect's own dependency list is designed to avoid.\n const centerOnElementRef = useRef(panZoom.centerOnElement);\n useEffect(() => {\n centerOnElementRef.current = panZoom.centerOnElement;\n });\n\n const { treeData, resolvedRootColor, svgWidth, svgHeight } = useMemo(() => {\n const root = buildTreeHierarchyStructure(d3, nodes);\n const originalTreeData = createTreeLayout(\n d3,\n root,\n orientation,\n depthSpacingScale,\n );\n const {\n svgWidth: width,\n svgHeight: height,\n highestVerticalCoordinate,\n rootDepthOffset,\n } = calculateSvgDimensions(\n d3,\n originalTreeData,\n orientation,\n hideRoot,\n depthSpacingScale,\n );\n const reshapedTreeData = setupTreeLayout(\n d3,\n originalTreeData,\n highestVerticalCoordinate,\n orientation,\n rootDepthOffset,\n width,\n depthSpacingScale,\n );\n\n return {\n treeData: reshapedTreeData,\n resolvedRootColor: rootColor ?? getGenreTreeColor(root.data.wikidataId),\n svgWidth: width,\n svgHeight: height,\n };\n }, [nodes, rootColor, orientation, hideRoot, depthSpacingScale]);\n\n // A reparent-in-progress node can belong to a *different* GenreTree instance (a different\n // root). Only the tree that actually contains it needs to block self/descendants as targets.\n const reparentForbiddenIds = useMemo(() => {\n if (!reparentingNodeId) return [];\n const reparentingHierarchyNode = treeData\n .descendants()\n .find((d) => d.data.id === reparentingNodeId);\n if (!reparentingHierarchyNode) return [];\n return reparentingHierarchyNode.descendants().map((d) => d.data.id);\n }, [treeData, reparentingNodeId]);\n\n const effectiveSelectedNodeId =\n selectedNodeIdProp !== undefined ? selectedNodeIdProp : (panel?.node.id ?? null);\n\n useEffect(() => {\n if (!svgRef.current) return;\n\n d3.select(svgRef.current).selectAll(\"*\").remove();\n\n renderTree(\n d3,\n svgRef,\n treeData,\n svgWidth,\n svgHeight,\n reparentingNodeId,\n reparentForbiddenIds,\n resolvedRootColor,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId) {\n void onReparent?.(reparentingNodeId, newParentId);\n }\n },\n onNodeClick: (data, event) => {\n const clickedElement = event.currentTarget as Element | null;\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n event.currentTarget as Element | null,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n additionalActions,\n playingNodeId,\n playState,\n },\n orientation,\n hideRoot,\n effectiveSelectedNodeId,\n );\n }, [\n treeData,\n svgWidth,\n svgHeight,\n reparentingNodeId,\n reparentForbiddenIds,\n resolvedRootColor,\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n orientation,\n hideRoot,\n effectiveSelectedNodeId,\n ]);\n\n // Mirrors the onNodeClick/onSelectNode handling: an externally-controlled selection (e.g. from a\n // search result) centers the viewport on that node and opens its info panel exactly as a direct\n // click would. Declared after the draw effect above so the node's group element already exists,\n // and skipped when `interactive` is false since the panel then belongs to an ancestor.\n useEffect(() => {\n if (!interactive || !selectedNodeIdProp) return;\n if (panel?.node.id === selectedNodeIdProp) return;\n const targetNode = index.byId.get(selectedNodeIdProp);\n const element = svgRef.current?.querySelector(\n `#group-${CSS.escape(selectedNodeIdProp)}`,\n );\n if (!targetNode || !element) return;\n centerOnElementRef.current(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(targetNode, element, viewportRef.current, { notify: false });\n }, [interactive, selectedNodeIdProp, index, panel]);\n\n useEffect(() => {\n const svgElement = svgRef.current;\n return () => {\n if (svgElement) {\n d3.select(svgElement).selectAll(\"*\").remove();\n }\n };\n }, []);\n\n // Starts the view fit to the whole tree instead of at scale 1 / pan (0, 0) — runs once the\n // tree's own draw effect above has populated svgRef, guarded so it never re-fires afterward\n // (e.g. on a subsequent nodes/layout change) and fights the user's own pan/zoom.\n const hasInitialFitRef = useRef(false);\n useEffect(() => {\n if (hasInitialFitRef.current || !interactive) return;\n const elements = queryTreeContentElements(svgRef.current);\n if (elements.length === 0) return;\n hasInitialFitRef.current = true;\n panZoom.fitToFrame(elements, { silent: true });\n });\n\n const svg = (\n <svg\n ref={svgRef}\n width={svgWidth}\n height={svgHeight}\n style={{ overflow: \"visible\", display: \"block\" }}\n />\n );\n\n if (!interactive) {\n // No own viewport, transform, or controls — an ancestor (e.g. GenreTreeWheel's shared stage)\n // supplies the entire pan/zoom transform for this tree and whatever else it's paired with.\n return <div className={className}>{svg}</div>;\n }\n\n return (\n <div\n ref={viewportRef}\n className={className}\n aria-busy={busy}\n style={\n {\n position: \"relative\",\n overflow: \"hidden\",\n containerType: \"inline-size\",\n width: \"100%\",\n height: \"100%\",\n // Everything inside this div (the tree stage, the info panel, the floating zoom controls)\n // is absolutely positioned, so nothing in-flow gives it a size of its own — it requires an\n // explicit-sized ancestor for width/height: 100% to resolve, and without one collapses to\n // 0 height, below ZOOM_FIT_PADDING. usePanZoom's fitToFrame already no-ops\n // rather than fit into a viewport that small (see ZOOM_FIT_PADDING guard in use-pan-zoom.ts),\n // so no minWidth/minHeight floor is applied here — flooring at the tree's own size used to\n // force this div larger than a smaller explicit-sized ancestor for any tree bigger than it,\n // which broke fitToFrame's viewport measurement and left content clipped against that\n // ancestor's actual (smaller) visible bounds.\n cursor: \"grab\",\n // Never \"auto\": a touchscreen two-finger pinch would otherwise never reach JS at all — the\n // browser consumes it as native page zoom before usePanZoom's pointer handlers see it.\n touchAction: panZoom.touchAction,\n } as React.CSSProperties\n }\n onPointerDown={panZoom.handlePointerDown}\n >\n {/* transform-origin: 0 0 so panX/panY/zoomScale compose in one consistent coordinate\n system — see use-pan-zoom.ts. */}\n <div\n ref={stageRef}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n transform: panZoom.transform,\n transformOrigin: \"0 0\",\n }}\n >\n {svg}\n </div>\n {panel && (\n <InfoPanel\n node={panel.node}\n // Mirrors tree-renderer.ts's isSubtreeCore(d) = hideRoot && d.depth >= 1. panel.node is\n // only ever set from a click on a rendered node, and the root (depth 0) is excluded from\n // rendering entirely when hideRoot is true, so panel.node is guaranteed to have a parent\n // whenever hideRoot is true — leaving hideRoot as the only variable, as with the children\n // below.\n fill={hideRoot ? resolvedRootColor : tintSurface(resolvedRootColor)}\n textColor={hideRoot ? ACCENT_TEXT_COLOR : TEXT_COLOR}\n parentNode={\n index.byId.get(panel.node.parentId ?? \"\")\n ? {\n node: index.byId.get(panel.node.parentId!)!,\n fill: hideRoot\n ? resolvedRootColor\n : tintSurface(resolvedRootColor),\n textColor: hideRoot ? ACCENT_TEXT_COLOR : TEXT_COLOR,\n }\n : null\n }\n childNodes={(index.childrenByParentId.get(panel.node.id) ?? [])\n .map((n) => ({\n node: n,\n // Mirrors tree-renderer.ts's isSubtreeCore(d) = hideRoot && d.depth >= 1 — every\n // direct child here has a parent, so depth >= 1 always holds, leaving hideRoot as\n // the only variable.\n fill: hideRoot\n ? resolvedRootColor\n : tintSurface(resolvedRootColor),\n textColor: hideRoot ? ACCENT_TEXT_COLOR : TEXT_COLOR,\n }))}\n ancestorNodes={index.ancestorsOf(panel.node.id).map(\n (n): InfoPanelChild => ({\n node: n,\n // Same fill/textColor expression as parentNode/childNodes above, including for\n // the root itself when the chain reaches it.\n fill: hideRoot ? resolvedRootColor : tintSurface(resolvedRootColor),\n textColor: hideRoot ? ACCENT_TEXT_COLOR : TEXT_COLOR,\n }),\n )}\n side={panel.side}\n onClose={closeNodeInfo}\n onSelectNode={(id) => {\n const targetNode = index.byId.get(id)!;\n const element = svgRef.current!.querySelector(\n `#group-${CSS.escape(id)}`,\n );\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(\n targetNode,\n element ?? viewportRef.current,\n viewportRef.current,\n );\n }}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n renderInfoPanelBody={renderInfoPanelBody}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n }\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n <FloatingControls\n panZoom={panZoom}\n orientation={controlsOrientation}\n leading={floatingControlsLeading}\n busy={busy}\n onFit={() => panZoom.fitToFrame(queryTreeContentElements(svgRef.current))}\n />\n </div>\n );\n}\n","import ReactDOMServer from \"react-dom/server\";\nimport { PiGraphFill } from \"react-icons/pi\";\nimport * as d3 from \"d3\";\n\nimport { GenreTreeNode } from \"./types\";\nimport {\n ACCENT_COLOR,\n ACCENT_TEXT_COLOR,\n CORNER_RADIUS,\n calculateNodeDimensions,\n Dimensions,\n getItemCountRange,\n ItemCountRange,\n} from \"./constants\";\n\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\n\n/** Builds the hierarchical tree structure from a flat node list. Each node's `itemCount` is\n * already its subtree total (see `GenreTreeNode.itemCount`), so it is used as-is, never summed. */\nexport function buildTreeHierarchyStructure(d3Lib: typeof import(\"d3\"), nodes: GenreTreeNode[]) {\n return d3Lib\n .stratify<GenreTreeNode>()\n .id((d) => d.id)\n .parentId((d) => d.parentId)(nodes);\n}\n\n/** A subtree root's count is the total of the whole subtree, so it's always that\n * subtree's own local maximum — meaning tree-renderer always sizes a hidden root against its own\n * subtree's item-count range, never the range a caller might be using to compare that root\n * against sibling roots elsewhere (e.g. a wheel's cross-root chip-sizing scale). Callers that need\n * to predict a hidden root's rendered width/height — to reserve matching clearance for it — must\n * mirror this same local-range computation rather than reusing their own cross-root\n * scale. */\nexport function calculateLocalRootDimensions(d3Lib: typeof import(\"d3\"), nodes: GenreTreeNode[]): Dimensions {\n const root = buildTreeHierarchyStructure(d3Lib, nodes);\n const range = getItemCountRange(root.descendants().map((d) => d.data));\n return calculateNodeDimensions(root.data.itemCount, range);\n}\n\n/** A wheel's tree-anchor clearance along one axis: how far past its own wheel-anchor point a\n * hidden root's mounted subtree needs to be pushed so its root->depth1 links stop at the visible\n * chip's outer edge instead of drawing across it. Two half-extents stack, not one — the hidden\n * root's own local half-extent (its link endpoint sits at its own invisible box's *center*, per\n * appendPaths' `d.x + WIDTH/2` convention, so clearing the box at all takes a full local\n * half-extent) plus the visible chip's own cross-root display half-extent (clearing the box only\n * reaches the chip's *center*, since that's where the anchor and the chip are both pinned — a\n * second half-extent, this time the chip's own displayed size, is what reaches its actual edge). */\nexport function calculateRootAnchorClearance(\n d3Lib: typeof import(\"d3\"),\n nodes: GenreTreeNode[],\n rootItemCount: number,\n rootItemCountRange: ItemCountRange,\n axis: keyof Dimensions,\n): number {\n const localExtent = calculateLocalRootDimensions(d3Lib, nodes)[axis];\n const displayExtent = calculateNodeDimensions(rootItemCount, rootItemCountRange)[axis];\n return (localExtent + displayExtent) / 2;\n}\n\n/** Adds the \"select as new parent\" overlay to a node while a reparent is in progress. */\nexport function addReparentTargetOverlay(\n d3Lib: typeof import(\"d3\"),\n parentNode: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n onReparentTargetSelect: (newParentId: string) => void,\n itemCountRange: ItemCountRange,\n) {\n const nodeId = (parentNode.datum() as D3Node).data.id;\n const nodeData = (parentNode.datum() as D3Node).data;\n const dimensions = calculateNodeDimensions(nodeData.itemCount, itemCountRange);\n\n let overlayGroup = parentNode.select<SVGGElement>(\"#select-as-new-parent-group-\" + nodeId);\n if (overlayGroup.empty()) {\n overlayGroup = parentNode\n .append(\"g\")\n .attr(\"id\", \"select-as-new-parent-group-\" + nodeId)\n .style(\"cursor\", \"pointer\")\n .on(\"mouseleave\", function () {\n d3Lib.select(this).remove();\n });\n\n overlayGroup\n .append(\"rect\")\n .attr(\"width\", dimensions.WIDTH)\n .attr(\"height\", dimensions.HEIGHT)\n .attr(\"x\", -dimensions.WIDTH / 2)\n .attr(\"y\", -dimensions.HEIGHT / 2)\n .attr(\"rx\", CORNER_RADIUS)\n .attr(\"ry\", CORNER_RADIUS)\n .attr(\"fill\", ACCENT_COLOR);\n\n overlayGroup\n .append(\"foreignObject\")\n .attr(\"width\", dimensions.WIDTH)\n .attr(\"height\", dimensions.HEIGHT)\n .attr(\"x\", -dimensions.WIDTH / 2)\n .attr(\"y\", -dimensions.HEIGHT / 2)\n .html(() =>\n ReactDOMServer.renderToString(\n <div className=\"gtv-reparent-target\">\n <PiGraphFill size={20} color={ACCENT_TEXT_COLOR} />\n <div className=\"gtv-reparent-target-label\" style={{ color: ACCENT_TEXT_COLOR }}>\n Select as new parent\n </div>\n </div>,\n ),\n )\n .on(\"click\", function (_event: MouseEvent, d: unknown) {\n onReparentTargetSelect((d as D3Node).data.id);\n });\n }\n\n return overlayGroup;\n}\n","import { hsl } from \"d3\";\nimport type { TreeIndex } from \"./tree-index\";\n\n// Visual theme tokens. All render code (tree-renderer.ts, d3-path-helper.ts,\n// NodeHelper.tsx) reads exclusively from this block, so the tree's whole look can be\n// swapped by editing only these values.\nexport const SURFACE_FILL = \"#FFFFFF\";\nexport const SURFACE_BORDER_COLOR = \"#E4E4E7\";\nexport const CORNER_RADIUS = 8;\nexport const ELEVATION = false;\n\nexport const CONNECTOR_COLOR = \"#D4D4D8\";\nexport const CONNECTOR_OPACITY = 1;\n\n// Radial wheel subtree links (renderPopSubtree) take a muted tintSurface of their root's color;\n// pop links are lighter than core ones, matching the pop/core node fill contrast. The white\n// Mainstream Pop center uses TEXT_MUTED_COLOR instead.\nexport const RADIAL_CORE_LINK_TINT_RATIO = 0.5;\nexport const RADIAL_POP_LINK_TINT_RATIO = 0.3;\n\nexport const TEXT_COLOR = \"#18181B\";\nexport const TEXT_MUTED_COLOR = \"#A1A1AA\";\n\nexport const ACCENT_COLOR = \"#4F46E5\";\nexport const ACCENT_TEXT_COLOR = \"#FFFFFF\";\n\n// The selected node (the one whose info panel is open) gets a heavier accent-colored border,\n// while every other node dims (see .gtv-node--dimmed in styles.css) — reuses ACCENT_COLOR\n// rather than a separate color so the highlight reads as the same \"accent\" language as the\n// reparent-target overlay (NodeHelper.tsx).\nexport const SELECTED_BORDER_WIDTH_RATIO = 2.5;\n\n// Tokens for the toolbar's inline icon row and its overflow menu — reuse the surface/text/\n// accent tokens above so both read as part of the same light, neutral card language.\nexport const DANGER_COLOR = \"#DC2626\";\nexport const MENU_ROW_HOVER_FILL = \"#F4F4F5\";\nexport const DANGER_ROW_HOVER_FILL = \"rgba(220, 38, 38, 0.08)\";\nexport const ACCENT_TINT_FILL = \"rgba(79, 70, 229, 0.08)\";\n\nexport const TOOLBAR_GAP = 2;\n\nexport const MENU_WIDTH = 130;\n\nexport const PER_TREE_ACCENT_DOT = true;\nexport const ACCENT_DOT_SIZE = 4;\n\n// Portion of a tree's root color mixed into SURFACE_FILL for its nodes' background tint (see\n// tintSurface) — matches ACCENT_TINT_FILL's 0.08 alpha above so both read as the same \"subtle\n// wash\" strength.\nexport const ROOT_TINT_RATIO = 0.08;\n\n// Tint ratio for a radial wheel's pop-sector nodes (see renderPopSubtree in\n// pop-core-radial-layout.ts) — stronger than ROOT_TINT_RATIO so pop nodes read as a lighter\n// shade of their root's own color rather than the near-white wash the toolbar/card surfaces use.\nexport const POP_SECTOR_TINT_RATIO = 0.8;\n\n// Share of its color a node in an underground-scene subtree keeps (the rest is mixed toward black).\nexport const UNDERGROUND_DARKEN_RATIO = 0.62;\n\n// Opacity of the darker background slice drawn behind an underground-scene subtree, layered over\n// the root's own sector wash.\nexport const UNDERGROUND_SECTOR_FILL_OPACITY = 0.15;\n\n// Outer radius (px) of that slice — matches the half-size of the 200000px root sector wash, so it\n// reads as running out to infinity.\nexport const SCENE_SECTOR_OUTER_RADIUS = 100000;\n\nexport const DEFAULT_NODE_COLOR = \"#4F46E5\";\n\n// The pop-core renderers (GenreTreeWheelRadialPopCore's wheel pivot, GenreTreeOutline's first\n// entry) single out this specific root by name and throw if it's missing.\nexport const CENTER_NODE_NAME = \"Mainstream Pop\";\n\n// Placeholder palette cycled where no real root exists (GenreTreeWheelSkeleton); real roots use\n// ROOT_GENRE_COLORS.\nexport const TREE_COLORS = [\n \"#4F46E5\", // Indigo\n \"#0D9488\", // Teal\n \"#D97706\", // Amber\n \"#E11D48\", // Rose\n \"#7C3AED\", // Violet\n \"#0891B2\", // Cyan\n \"#059669\", // Emerald\n \"#EA580C\", // Orange\n];\n\n/** Blends `hex` into SURFACE_FILL at `ratio` (0-1) and returns an opaque hex color — used for a\n * node's root-color background tint. Opaque (rather than an rgba fill) so the tint reads the\n * same regardless of what the host page renders behind the tree. */\nexport function tintSurface(hex: string, ratio: number = ROOT_TINT_RATIO): string {\n const channel = (offset: number) => parseInt(hex.slice(offset, offset + 2), 16);\n const mix = (value: number) => Math.round(value * ratio + 255 * (1 - ratio));\n const toHex = (value: number) => value.toString(16).padStart(2, \"0\");\n return `#${toHex(mix(channel(1)))}${toHex(mix(channel(3)))}${toHex(mix(channel(5)))}`;\n}\n\n// Opacity of a wheel view's per-root background sector fill (see buildWheelSectorGradient /\n// buildSectorClipPathPolygon) — light enough to read as a wash behind chips/dividers/subtrees\n// rather than competing with them for attention.\nexport const ROOT_SECTOR_FILL_OPACITY = 0.15;\n\n/** Converts an opaque \"#RRGGBB\" color (e.g. a ROOT_GENRE_COLORS entry) to an \"rgba()\" string at\n * `opacity` (0-1) — unlike tintSurface, which stays opaque by blending toward a fixed background,\n * this produces a true transparent fill for layering over arbitrary content (e.g. a wheel's\n * sector-fill wash, which sits behind chips/dividers/subtrees of varying color). */\nexport function hexToRgba(hex: string, opacity: number): string {\n const channel = (offset: number) => parseInt(hex.slice(offset, offset + 2), 16);\n return `rgba(${channel(1)}, ${channel(3)}, ${channel(5)}, ${opacity})`;\n}\n\n// Fixed per-root colors keyed by the root's wikidata id (a QID or a synthetic \"LOCAL:\" key), so a\n// genre keeps its color across environments and renames. Mainstream Pop (Q37073) is deliberately\n// absent: the pop-core layouts render it white at the center.\nexport const ROOT_GENRE_COLORS: Readonly<Record<string, string>> = {\n \"LOCAL:blues-rock\": \"#DC2626\", // Red\n \"LOCAL:reggae-dub\": \"#16A34A\", // Green\n \"LOCAL:disco-funk\": \"#7C3AED\", // Violet\n Q11401: \"#B8860B\", // Hip-hop, deep gold\n Q9778: \"#2563EB\", // Electronic, blue\n Q1583807: \"#92400E\", // Art music (classical), brown\n Q8341: \"#CA8A04\", // Jazz, mustard\n Q45981: \"#4F46E5\", // Rhythm and blues, indigo\n Q43343: \"#0D9488\", // Folk, teal\n Q54365: \"#0891B2\", // Ska, cyan\n Q1640319: \"#475569\", // Experimental, slate\n};\n\nexport const FALLBACK_ROOT_COLOR = \"#6B7280\";\n\n/** Maps a root node's wikidata id to its fixed color, or FALLBACK_ROOT_COLOR when unmapped/absent. */\nexport function getGenreTreeColor(wikidataId: string | null | undefined): string {\n return (wikidataId && ROOT_GENRE_COLORS[wikidataId]) || FALLBACK_ROOT_COLOR;\n}\n\n// A zoomed view's ring roots are descendants of a top-level root, so they take a variation of the\n// color of the sector they were zoomed from: hue spread across +-this many degrees over siblings.\nconst CHILD_HUE_SPREAD_DEG = 30;\nconst CHILD_LIGHTNESS_STEP = 0.06;\nconst CHILD_MIN_LIGHTNESS = 0.25;\nconst CHILD_MAX_LIGHTNESS = 0.6;\n\n/** Color of `nodeId`: a top-level root's fixed color, otherwise a variation of its parent's color\n * (hue spread and alternating lightness by position among its non-pop siblings). A pure function of\n * the full tree, so a genre keeps its color at every zoom depth. */\nexport function getGenreNodeColor(index: Pick<TreeIndex, \"byId\" | \"childrenByParentId\">, nodeId: string): string {\n const node = index.byId.get(nodeId);\n if (!node) return FALLBACK_ROOT_COLOR;\n if (node.parentId === null) return getGenreTreeColor(node.wikidataId);\n const siblings = (index.childrenByParentId.get(node.parentId) ?? []).filter((sibling) => sibling.side !== \"pop\");\n const position = Math.max(0, siblings.findIndex((sibling) => sibling.id === nodeId));\n const base = hsl(getGenreNodeColor(index, node.parentId));\n const hueOffset =\n siblings.length > 1 ? -CHILD_HUE_SPREAD_DEG + (2 * CHILD_HUE_SPREAD_DEG * position) / (siblings.length - 1) : 0;\n const lightness = base.l + (position % 2 === 0 ? -CHILD_LIGHTNESS_STEP : CHILD_LIGHTNESS_STEP);\n return hsl(\n base.h + hueOffset,\n base.s,\n Math.min(CHILD_MAX_LIGHTNESS, Math.max(CHILD_MIN_LIGHTNESS, lightness)),\n ).formatHex();\n}\n\ntype GenreStyleIndex = Pick<TreeIndex, \"byId\" | \"childrenByParentId\" | \"ancestorsOf\">;\n\n/** Whether `nodeId` is the center \"Mainstream Pop\" root or one of its descendants. */\nexport function isMainstreamPopLineage(index: GenreStyleIndex, nodeId: string): boolean {\n const topRoot = index.ancestorsOf(nodeId)[0] ?? index.byId.get(nodeId);\n return topRoot?.name === CENTER_NODE_NAME;\n}\n\n/** Mixes `hex` toward black: `ratio` 1 keeps it, 0 is black. Counterpart of `tintSurface`. */\nexport function shadeSurface(hex: string, ratio: number): string {\n const channel = (offset: number) => parseInt(hex.slice(offset, offset + 2), 16);\n const toHex = (value: number) => Math.round(value * ratio).toString(16).padStart(2, \"0\");\n return `#${toHex(channel(1))}${toHex(channel(3))}${toHex(channel(5))}`;\n}\n\n/** Fill/text color of `nodeId` in the pop-core wheel: the Mainstream Pop lineage is a white tint\n * with dark text; any other genre is its own `getGenreNodeColor`, tinted when it sits on a pop\n * branch. Like `getGenreNodeColor`, independent of the zoom frame the node renders in. */\nexport function getGenreNodeStyle(index: GenreStyleIndex, nodeId: string): { fill: string; textColor: string } {\n if (isMainstreamPopLineage(index, nodeId)) {\n return { fill: tintSurface(\"#ffffff\", POP_SECTOR_TINT_RATIO), textColor: TEXT_COLOR };\n }\n const node = index.byId.get(nodeId);\n const lineageBelowTopRoot = node ? [...index.ancestorsOf(nodeId), node].slice(1) : [];\n const isPop = lineageBelowTopRoot.some((n) => n.side === \"pop\");\n const color = getGenreNodeColor(index, nodeId);\n return { fill: isPop ? tintSurface(color, POP_SECTOR_TINT_RATIO) : color, textColor: ACCENT_TEXT_COLOR };\n}\n\nexport interface Dimensions {\n WIDTH: number;\n HEIGHT: number;\n}\n\nexport const RECT_BASE_DIMENSIONS: Dimensions = {\n WIDTH: 180,\n HEIGHT: 30,\n};\n\n// Dynamic sizing configuration\nexport const MIN_NODE_WIDTH = 180;\nexport const MAX_NODE_WIDTH = 700;\nexport const MIN_NODE_HEIGHT = 35;\nexport const MAX_NODE_HEIGHT = 120;\n\n// Reserved space to a node's right/top/bottom for its toolbar icon row and overflow menu,\n// so the tree layout leaves room for them instead of packing nodes edge-to-edge. Width covers\n// the overflow menu's own width plus the gap the toolbar renders it at (see the `+ 4` x-offset\n// by the former hover toolbar), since the menu is wider than the icon row itself.\nexport const TOOLBAR_MENU_X_GAP = 4;\nexport const ACTIONS_OVERLAY_WIDTH = MENU_WIDTH + TOOLBAR_MENU_X_GAP;\nexport const ACTIONS_OVERLAY_HEIGHT = RECT_BASE_DIMENSIONS.HEIGHT * 7;\n\n// Layout slot size, not a rendered node's actual size (see calculateNodeDimensions) — every\n// node's slot has to fit the largest a node can render at, or a high-itemCount node overflows\n// its neighbors' slots and overlaps them.\nexport const NODE_DIMENSIONS: Dimensions = {\n WIDTH: MAX_NODE_WIDTH + ACTIONS_OVERLAY_WIDTH,\n HEIGHT: MAX_NODE_HEIGHT,\n};\n\nexport const HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES = 5;\nexport const VERTICAL_SEPARATION_BETWEEN_RECTANGLES = 20;\n// 0.8x: depth-axis spacing only (see HORIZONTAL_SEPARATION_BETWEEN_NODES and\n// VERTICAL_ORIENTATION_DEPTH_SEPARATION below), tightened 20% at the consumer's request.\nconst DEPTH_AXIS_SPACING_FACTOR = 0.8;\nexport const HORIZONTAL_SEPARATION_BETWEEN_NODES =\n (NODE_DIMENSIONS.WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES) * DEPTH_AXIS_SPACING_FACTOR;\nexport const VERTICAL_SEPARATION_BETWEEN_NODES = NODE_DIMENSIONS.HEIGHT + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;\n\n// The depth-axis step for a vertical-orientation tree (root-to-leaf growth). Kept distinct from\n// VERTICAL_SEPARATION_BETWEEN_NODES, which horizontal-orientation trees also reuse as their\n// breadth (sibling) axis step — doubling that constant directly would have also doubled sibling\n// spacing in horizontal trees, not just the gap between depths.\nexport const VERTICAL_ORIENTATION_DEPTH_SEPARATION = VERTICAL_SEPARATION_BETWEEN_NODES * 2 * DEPTH_AXIS_SPACING_FACTOR;\n\n// Same-depth siblings in a vertical-orientation tree sit side by side on-screen, so this axis\n// doesn't need HORIZONTAL_SEPARATION_BETWEEN_NODES's baked-in toolbar/menu headroom — that's\n// only for the depth axis, where a hovered node's toolbar pops into the space before the next\n// generation. Reserving that much between every sibling pair made the wheel's tree read far\n// more spread out than its cards actually are.\nexport const SIBLING_SEPARATION_BETWEEN_NODES = MAX_NODE_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES;\n\n// Utility functions for dynamic node sizing\nexport interface ItemCountRange {\n min: number;\n max: number;\n}\n\n/** The min/max itemCount across a set of nodes — the reference points calculateNodeDimensions\n * scales every node's size against, so a node's size reflects its item count relative to its\n * peers rather than an absolute, arbitrarily-chosen scale. */\nexport function getItemCountRange(nodes: Array<{ itemCount: number }>): ItemCountRange {\n if (nodes.length === 0) return { min: 0, max: 0 };\n const counts = nodes.map((node) => node.itemCount);\n return { min: Math.min(...counts), max: Math.max(...counts) };\n}\n\n/** Log-scaled position of itemCount within `range`, normalized to [0, 1] — the node with the\n * lowest itemCount in range always sits at 0 and the highest always at 1, regardless of the\n * actual counts involved. A degenerate range (every node sharing one count) falls back to 0.\n * Real track counts commonly span tens to tens of thousands: a linear scale would flatten\n * nearly every node down near the minimum and let only the very largest stand out, so position\n * is computed on a log scale instead — equal *ratios* of item count grow a node by roughly\n * equal amounts, not equal absolute differences. log1p (not log) so an itemCount of 0 is finite. */\nexport function logarithmicPosition(itemCount: number, range: ItemCountRange): number {\n const { min, max } = range;\n const logMin = Math.log1p(Math.max(min, 0));\n const logMax = Math.log1p(Math.max(max, 0));\n const logValue = Math.log1p(Math.max(itemCount, 0));\n return logMax > logMin ? (logValue - logMin) / (logMax - logMin) : 0;\n}\n\n/** Maps itemCount's log-scaled position within `range` onto [MIN_NODE_*, MAX_NODE_*]. */\nexport function calculateNodeDimensions(itemCount: number, range: ItemCountRange): Dimensions {\n const t = logarithmicPosition(itemCount, range);\n\n return {\n WIDTH: Math.round(MIN_NODE_WIDTH + t * (MAX_NODE_WIDTH - MIN_NODE_WIDTH)),\n HEIGHT: Math.round(MIN_NODE_HEIGHT + t * (MAX_NODE_HEIGHT - MIN_NODE_HEIGHT)),\n };\n}\n\nexport const NODE_MIN_BORDER_WIDTH = 1;\nexport const NODE_MAX_BORDER_WIDTH = 4;\n\n/** Border width of a node (card or dot), on the same log scale as its size; the selected node's\n * is SELECTED_BORDER_WIDTH_RATIO times its own width, so a selected small node can still be\n * thinner than an unselected large one. */\nexport function calculateNodeBorderWidth(itemCount: number, range: ItemCountRange, selected = false): number {\n const width =\n NODE_MIN_BORDER_WIDTH + logarithmicPosition(itemCount, range) * (NODE_MAX_BORDER_WIDTH - NODE_MIN_BORDER_WIDTH);\n return selected ? width * SELECTED_BORDER_WIDTH_RATIO : width;\n}\n\nexport const SELECTED_BORDER_TO_RADIUS_RATIO = 0.2;\n\n/** Selected-node border of a dot or chip, proportional to its radius. */\nexport function calculateSelectedNodeBorderWidth(radius: number): number {\n return radius * SELECTED_BORDER_TO_RADIUS_RATIO;\n}\n\nexport const LINK_TO_NODE_HEIGHT_RATIO = 1 / 6;\n\n/** Stroke width of the link leading to a node: a fixed fraction of that node's rendered height,\n * so a branch stays the same order of magnitude as the chip it leads to. */\nexport function calculateLinkWidth(itemCount: number, range: ItemCountRange): number {\n return calculateNodeDimensions(itemCount, range).HEIGHT * LINK_TO_NODE_HEIGHT_RATIO;\n}\n\n// The node-height fraction its font size renders at, interpolated by the same log-scaled t as\n// calculateNodeDimensions — small nodes get a smaller fraction of their (already small) height so\n// their label doesn't dominate the card, while the largest nodes reach a full half.\nconst MIN_NODE_FONT_HEIGHT_RATIO = 0.3;\nconst MAX_NODE_FONT_HEIGHT_RATIO = 0.5;\n\n/** A fraction of the node's own box height — keeps the label's font size proportional to its\n * node's rendered size, rather than interpolated separately over its own min/max range. */\nexport function calculateNodeFontSize(itemCount: number, range: ItemCountRange): number {\n const t = logarithmicPosition(itemCount, range);\n const { HEIGHT } = calculateNodeDimensions(itemCount, range);\n const ratio = MIN_NODE_FONT_HEIGHT_RATIO + t * (MAX_NODE_FONT_HEIGHT_RATIO - MIN_NODE_FONT_HEIGHT_RATIO);\n\n return Math.round(HEIGHT * ratio);\n}\n\n/** Layout-unit font size that renders a dot label at its on-screen size (LABEL_SCREEN_*_PX, log\n * scale like calculateNodeFontSize) under `zoomScale`. */\nexport function calculateLabelFontSizeAtZoom(itemCount: number, range: ItemCountRange, zoomScale: number): number {\n const t = logarithmicPosition(itemCount, range);\n return (LABEL_SCREEN_MIN_PX + t * (LABEL_SCREEN_MAX_PX - LABEL_SCREEN_MIN_PX)) / zoomScale;\n}\n\n/** A dot label's map-style importance: its log-scaled itemCount, lowered by\n * LABEL_DEPTH_RANK_PENALTY per level below the first under its root (`depth` 1), so a level is\n * named before the levels under it unless a deeper genre is much bigger. */\nexport function calculateLabelRank(itemCount: number, range: ItemCountRange, depth: number): number {\n return logarithmicPosition(itemCount, range) - Math.max(0, depth - 1) * LABEL_DEPTH_RANK_PENALTY;\n}\n\n/** Whether a label of `rank` (see calculateLabelRank) is named at `zoomScale` (see\n * LABEL_MIN_RANK_AT_MIN_ZOOM); the threshold falls linearly in log-zoom from ZOOM_MIN_SCALE to\n * ZOOM_FOCUS_SCALE, and below 0 past it so deep levels keep appearing while zooming further in. */\nexport function isLabelRankedForZoom(rank: number, zoomScale: number): boolean {\n const zoomOut = Math.log(ZOOM_FOCUS_SCALE / zoomScale) / Math.log(ZOOM_FOCUS_SCALE / ZOOM_MIN_SCALE);\n return rank >= Math.min(1, zoomOut) * LABEL_MIN_RANK_AT_MIN_ZOOM;\n}\n\n/** Root chip label size: its own layout size, floored so it never renders below\n * ROOT_LABEL_MIN_SCREEN_PX on screen — every root stays named at any zoom. */\nexport function calculateRootLabelFontSize(layoutFontSize: number, zoomScale: number): number {\n return Math.max(layoutFontSize, ROOT_LABEL_MIN_SCREEN_PX / zoomScale);\n}\n\nexport const NODE_DOT_MIN_RADIUS = 3;\nexport const NODE_DOT_MAX_RADIUS = 24;\nexport const NODE_DOT_LABEL_GAP = 6;\n// Half-width every node reserves on its ring when the radial wheels measure a subtree's angular\n// width: identical for all nodes, so layout depends on tree topology only and never on the size\n// metric (itemCount) that drives how big a dot actually renders.\nexport const NODE_LAYOUT_SLOT_RADIUS = 8;\n\n/** Radius of a radial-wheel subgenre dot, on the same log scale as its label's font size\n * (calculateNodeFontSize) so a bigger label always sits on a bigger dot. */\nexport function calculateNodeDotRadius(itemCount: number, range: ItemCountRange): number {\n const minArea = NODE_DOT_MIN_RADIUS ** 2;\n const maxArea = NODE_DOT_MAX_RADIUS ** 2;\n return Math.sqrt(minArea + logarithmicPosition(itemCount, range) * (maxArea - minArea));\n}\n\n// GenreTreeWheel tokens. Chips are spread evenly around the full circle (see getChipAngle in\n// wheel-geometry.ts). The wheel's reserved bottom strip needs to fit more than just the bare\n// circle (2 * WHEEL_RADIUS): a chip is centered *on* its point on that circle, so at the circle's\n// lowest point a chip's own half-height still sticks out past the diameter. Reserving that extra\n// half-height keeps every chip fully on screen, not just the ones nearest the top.\nexport const WHEEL_RADIUS = 260;\nexport const WHEEL_VIEWPORT_HEIGHT = WHEEL_RADIUS * 2 + MAX_NODE_HEIGHT / 2;\n\n// GenreTreeWheelRadialPopCore tokens. This renderer's circle must fit a full interactive pop\n// subtree inside it (not just chip clearance like the plain wheel), so it starts from a bigger\n// base floor than WHEEL_RADIUS even when no root has a pop side — leaving room to grow\n// further per calculatePopSubtreeRadialExtent (pop-core-radial-layout.ts) once one does.\nexport const WHEEL_POP_CORE_RADIUS = 945;\n\n// Radial (depth-axis) step for a pop subtree's polar layout — analogous to\n// VERTICAL_ORIENTATION_DEPTH_SEPARATION for the cartesian renderers, but expressed directly as a\n// radius increment per depth since pop-core-radial-layout.ts positions nodes in polar coordinates.\nexport const POP_TREE_DEPTH_RADIAL_SPACING = NODE_DIMENSIONS.WIDTH * 0.35;\n\n// Suggested size for the fixed-size ancestor any GenreTree/GenreTreeWheel variant requires (see\n// GenreTreeWheel's own doc comment): enough room for a typically-deep tree, or the wheel's chip\n// strip plus a typically-deep tree above it. Not derived from WHEEL_VIEWPORT_HEIGHT/WHEEL_RADIUS\n// — those size the wheel-of-chips only, not the variable-depth tree growing above it.\nexport const DEFAULT_FRAME_WIDTH = 1200;\nexport const DEFAULT_FRAME_HEIGHT = 750;\n\nexport const WHEEL_ROTATION_TRANSITION_MS = 500;\nexport const WHEEL_ROTATION_EASING = \"cubic-bezier(0.22, 1, 0.36, 1)\";\n\n// Zoom/pan tokens (see zoom-pan.ts). Bounds keep the tree from shrinking past legibility or\n// growing so large that a single wheel tick jumps an unreasonable amount.\nexport const ZOOM_MIN_SCALE = 0.05;\nexport const ZOOM_MAX_SCALE = 3;\n// Exponent multiplier applied to a trackpad ctrl+wheel/pinch event's deltaY (see\n// classifyWheelEvent in zoom-pan.ts — physical mouse wheel notches use ZOOM_BUTTON_SCALE_STEP\n// instead) — small because deltaY is typically tens to hundreds of pixels per tick, and exp()\n// amplifies fast. Raised repeatedly (0.0015 -> 0.003 -> 0.006 -> 0.012 -> 0.03): even with the\n// per-event animation delay removed, trackpad zoom still felt less sensitive than Google Maps'.\nexport const ZOOM_WHEEL_SCALE_SPEED = 0.03;\n// Caps the exponent (currentScale's multiplier is exp(this)) any single wheel/trackpad event can\n// apply, regardless of ZOOM_WHEEL_SCALE_SPEED. Researched d3-zoom's own wheel handling (the\n// library this project is built on) and Mapbox GL's scroll-zoom: both scale multiplicatively per\n// event exactly like computeZoomScale does, with no special curve to counter it — \"a faster swipe\n// zooms more\" is inherent to any proportional response, not a bug, since deltaY already grows with\n// gesture speed. What those implementations don't hit is an occasional very fast swipe producing a\n// runaway multiplicative jump in one single event; this cap bounds that per-event ratio to\n// exp(0.5) ≈ 1.65x without touching the feel of normal-speed gestures (which stay well under it).\nexport const ZOOM_WHEEL_MAX_EXPONENT = 0.5;\n// Multiplicative step applied per click of the zoom in/out buttons — a fallback control for\n// ctrl+scroll/pinch, which some trackpad/OS/browser combinations never translate into a\n// ctrlKey wheel event at all.\nexport const ZOOM_BUTTON_SCALE_STEP = 1.2;\n// Exponent applied to a touch pinch gesture's finger-distance ratio, amplifying the scale change\n// per unit of finger travel — touch pinch reaches JS as raw pointer events (see use-pan-zoom.ts),\n// with no OS-level amplification like trackpad pinch gets, so a 1:1 ratio feels sluggish. Lowered\n// from 1.5: on Android touchscreens that amplification made pinch-zoom feel too fast/twitchy.\nexport const ZOOM_PINCH_SCALE_SPEED = 1;\n// Duration (ms) of the eased glide between physical-mouse-wheel notches (see classifyWheelEvent\n// in zoom-pan.ts) — mirrors Google Maps' smooth zoom-and-settle for that chunky, low-frequency\n// input. Trackpad ctrl+wheel/pinch is applied instantly instead (see handleWheel in\n// use-pan-zoom.ts): it's already a continuous, high-frequency stream, so easing each event would\n// only stack added latency behind it rather than smooth anything.\nexport const ZOOM_ANIMATION_DURATION_MS = 220;\n// Breathing room (px) kept around content when \"fit to frame\" computes a scale — content is\n// never scaled to touch the viewport's edges exactly.\nexport const ZOOM_FIT_PADDING = 40;\n// Minimum scale a clicked node is zoomed/centered to (see usePanZoom's centerOnElement) — a\n// comfortable reading scale: zooms in when the user is farther out, but never zooms out when\n// they're already closer.\n// 0.2 (not 1): full scale read as \"too zoomed in\" once clicked, cropping most of the surrounding tree.\nexport const ZOOM_FOCUS_SCALE = 0.2;\n// On-screen font size (px) of radial dot labels, smallest to biggest genre: labels keep this size\n// at every zoom like place names on a map, so zooming out hides small labels (bigger in layout\n// units, they collide more) and zooming in brings them back.\nexport const LABEL_SCREEN_MIN_PX = 11;\nexport const LABEL_SCREEN_MAX_PX = 22;\n// Minimum log-scaled itemCount position (0 = smallest genre, 1 = biggest) a label needs to be shown\n// at ZOOM_MIN_SCALE, easing to 0 at ZOOM_FOCUS_SCALE: like a map, a far-out view names only the\n// main genres even where smaller labels would fit.\nexport const LABEL_MIN_RANK_AT_MIN_ZOOM = 0.6;\n// Rank (see calculateLabelRank) a label loses per hierarchy level below its root's direct children.\nexport const LABEL_DEPTH_RANK_PENALTY = 0.15;\nexport const ROOT_LABEL_MIN_SCREEN_PX = 15;\n// Quiet time (ms) after the last zoom change before dot labels are re-placed for the new scale.\nexport const LABEL_RELAYOUT_DEBOUNCE_MS = 120;\n// On-screen font size (px) of the zoomed-in root's region label: placed at TARGET, re-placed once\n// zooming drifts it outside [MIN, MAX] (see WheelRadialPopCoreCore).\nexport const REGION_LABEL_TARGET_PX = 28;\nexport const REGION_LABEL_MIN_PX = 16;\nexport const REGION_LABEL_MAX_PX = 48;\n// Duration (ms) of the eased pan+zoom glide centerOnElement flies to a clicked node with — longer\n// than ZOOM_ANIMATION_DURATION_MS's wheel-notch glide since this one also covers a pan distance,\n// not just a scale change, and should read as one deliberate \"fly to\" (Google Maps' own\n// click-to-center glide) rather than an instant jump.\nexport const CENTER_ON_ELEMENT_DURATION_MS = 450;\n// How long a single click waits for a second one before acting (see defer-click.ts).\nexport const DOUBLE_CLICK_DELAY_MS = 250;\n// Minimum content (px) usePanZoom's pan clamp always keeps on-screen along each axis — dragging\n// or wheel-panning can bring the tree to this close to sliding fully out of the viewport, but no\n// closer, so the user can never lose it entirely and have no visible edge to drag back from.\nexport const PAN_MIN_VISIBLE_PX = 80;\n\n// Idle time after the last wheel/drag/pinch event before usePanZoom commits its imperatively\n// tracked pan/zoom into React state (re-rendering and re-running the zoom-dependent label effects).\nexport const PAN_ZOOM_SETTLE_MS = 100;\n\n// After usePanZoom applies a `defaultViewport`, centerOnElement is ignored for this long so the\n// initial `selectedNodeId` (which centers on mount) doesn't glide away from the restored view.\nexport const DEFAULT_VIEWPORT_CENTER_GUARD_MS = 1000;\n\n// Fixed width (px) of the node info panel (see InfoPanel.tsx) — also the collision threshold\n// resolveInfoPanelSide compares a clicked node's screen rect against, so the panel never renders\n// directly on top of the node that opened it.\nexport const INFO_PANEL_WIDTH = 280;\n","import type { ReactNode } from \"react\";\n\nexport interface GenreTreeNode {\n id: string;\n parentId: string | null;\n name: string;\n /** Subtree total: this node's own items plus all its descendants'. Parents must already include\n * their children's counts — the library displays and sizes by this value as-is, never summing. */\n itemCount: number;\n /** Whether add-child, rename, delete and reparent actions apply to this node. Defaults to true. */\n actionable?: boolean;\n /** Meaningful only for a root's direct children: \"core\" (default when unset) marks the\n * required non-pop branch, \"pop\" marks the optional pop/crossover branch. Ignored everywhere\n * else. Only interpreted by GenreTreeWheelRadialPopCore. */\n side?: \"core\" | \"pop\";\n /** A root kept apart from the tree (e.g. an uncategorized bucket). Only interpreted by\n * GenreTreeOutline, which lists it in its own section after the tree. */\n detached?: boolean;\n /** Stable genre key (a wikidata QID or a synthetic \"LOCAL:\" key). Read on root nodes only, to\n * pick the root's fixed color (see getGenreTreeColor). */\n wikidataId?: string | null;\n /** A grouping node that no item is filed under directly. Read by InfoPanel (badge), GenreTreeOutline\n * and the stacked/wheel node labels (italic); the pop-core labels ignore it. */\n umbrella?: boolean;\n /** Which scene a genre belongs to. GenreTreeWheelRadialPopCore draws an \"underground\" node and its\n * descendants in a darker shade of their usual color, over a darker background slice. */\n scene?: \"commercial\" | \"underground\";\n}\n\nexport type GenreTreePlayState = \"playing\" | \"paused\" | \"loading\";\n\n/** \"stacked\" is the linear/card tree (GenreTree); \"wheel\"/\"pop-core\" are the radial layouts\n * (GenreTreeWheel* family); \"outline\" is the nested-list text view (GenreTreeOutline). Shared by\n * GenreTreeViewSkeleton so a consumer's loading state can match whichever real layout it's about\n * to render. */\nexport type GenreTreeViewMode = \"stacked\" | \"wheel\" | \"pop-core\" | \"outline\";\n\n/** \"horizontal\" grows children rightward from a left root (default). \"vertical\" grows children\n * upward from a bottom-anchored root — used by GenreTreeWheel. \"horizontal-anchored\" grows\n * rightward like \"horizontal\" but anchors the root at a fixed local coordinate\n * (0, svgHeight / 2) regardless of tree shape, instead of \"horizontal\"'s unanchored top-aligned\n * bounding box — used by GenreTreeWheelRight. \"vertical-flipped\" mirrors \"vertical\": children\n * grow downward from a top-anchored root. \"horizontal-anchored-flipped\" mirrors\n * \"horizontal-anchored\": children grow leftward from a root anchored at (svgWidth, svgHeight / 2).\n * The four anchored/mirrored variants are used by GenreTreeWheelRadial's top/right/bottom/left\n * cardinals respectively (vertical / horizontal-anchored / vertical-flipped /\n * horizontal-anchored-flipped). */\nexport type TreeOrientation =\n | \"horizontal\"\n | \"vertical\"\n | \"horizontal-anchored\"\n | \"vertical-flipped\"\n | \"horizontal-anchored-flipped\";\n\n/** True for the two orientations whose depth axis is Y (root-to-leaf grows vertically) rather\n * than X. */\nexport function isVerticalOrientation(orientation: TreeOrientation): boolean {\n return orientation === \"vertical\" || orientation === \"vertical-flipped\";\n}\n\n/** +1 if depth increases in the positive direction of its axis (down for \"vertical-flipped\",\n * right for \"horizontal\"/\"horizontal-anchored\"); -1 if it increases in the negative direction (up\n * for \"vertical\", left for \"horizontal-anchored-flipped\"). Determines which side of a card is\n * \"toward the leaves\" so the hover toolbar/hit-rect always extends that way, never toward the\n * parent. */\nexport function depthAxisSign(orientation: TreeOrientation): 1 | -1 {\n if (orientation === \"vertical\" || orientation === \"horizontal-anchored-flipped\") return -1;\n return 1;\n}\n\nexport interface GenreTreeAction {\n key: string;\n icon: (node: GenreTreeNode) => ReactNode;\n label: (node: GenreTreeNode) => string;\n onClick: (event: React.MouseEvent, node: GenreTreeNode) => void;\n enabled?: (node: GenreTreeNode) => boolean;\n danger?: boolean;\n /** \"primary\" = inline icon-row action (old upload slot); \"overflow\" = kebab-menu action\n * (old rename/delete slot). Defaults to \"overflow\". */\n placement?: \"primary\" | \"overflow\";\n}\n\n/** How the mouse wheel drives an interactive renderer's pan/zoom viewport — see\n * `GenreTreeProps.wheelZoom`. */\nexport type WheelZoomMode = \"always\" | \"modifier\";\n\n/** Layout of the floating zoom/pop/fit controls — see `GenreTreeProps.controlsOrientation`. */\nexport type ControlsOrientation = \"horizontal\" | \"vertical\";\n\n/** The pan/zoom view as the content-space point shown at the viewport's center plus the scale —\n * independent of the container's pixel size, so a saved view lands on the same spot in any window. */\nexport interface GenreTreeViewport {\n x: number;\n y: number;\n zoom: number;\n}\n\n/** The info panel's built-in sections, handed to `renderInfoPanelBody` for placement. Each is\n * `null` when it has nothing to show. */\nexport interface InfoPanelSections {\n /** The node's action toolbar — null when `showToolbar` is off or a reparent is in progress. */\n toolbar: ReactNode;\n /** Ancestor chips, root-first — null for a root. */\n ancestors: ReactNode;\n /** Parent, node and children mini tree. */\n family: ReactNode;\n /** The `itemCountLabel` row. */\n itemCount: ReactNode;\n}\n\n/** The info panel body a `renderInfoPanelBody` returns. */\nexport interface InfoPanelBody {\n /** The wide column when expanded; on top when compact. */\n primary: ReactNode;\n /** The narrow left column when expanded; below `primary` when compact. Omit (or null) for a\n * single column. */\n aside?: ReactNode;\n}\n\nexport interface GenreTreeProps {\n nodes: GenreTreeNode[];\n /** Label for the info panel's `itemCount` row (e.g. \"Song count\"); the library can't know what unit is counted. */\n itemCountLabel: string;\n className?: string;\n rootColor?: string;\n orientation?: TreeOrientation;\n /** Omits the root node's own card (and its toolbar) from rendering, keeping only its\n * connecting paths to its children — used when the root is represented elsewhere, e.g.\n * GenreTreeWheel's chip. Defaults to false. */\n hideRoot?: boolean;\n playingNodeId?: string | null;\n playState?: GenreTreePlayState;\n /** Id of the node currently being reassigned to a new parent (owned by the consumer, since it can span multiple GenreTree instances). */\n reparentingNodeId?: string | null;\n onPlayPause?: (nodeId: string) => void;\n onAddChild?: (parentId: string) => void;\n onRenameRequest?: (node: GenreTreeNode) => void;\n onDeleteRequest?: (node: GenreTreeNode) => void;\n /** Fired when the user clicks \"Change parent\" on a node — the consumer should set `reparentingNodeId` to this node's id. */\n onReparentRequest?: (node: GenreTreeNode) => void;\n /** Fired when the user picks a target node while `reparentingNodeId` is set. */\n onReparent?: (nodeId: string, newParentId: string) => void | Promise<void>;\n /** Fired when the user clicks a node's body (its card, or its chip in the wheel renderers) —\n * anywhere but a toolbar/menu action or, while `reparentingNodeId` is set, the reparent-target\n * overlay. The library itself opens its own read-only info panel for the clicked node\n * alongside this callback; the consumer owns any additional selection state or highlighting. */\n onNodeClick?: (node: GenreTreeNode, event: MouseEvent) => void;\n /** Fired when the pointer enters a node (or, in `GenreTreeOutline`, its name receives focus) —\n * e.g. to prefetch whatever the consumer loads on click. Fired by `GenreTreeOutline` and\n * `GenreTreeWheelRadialPopCore`; accepted and ignored by the other renderers. */\n onNodeHover?: (node: GenreTreeNode) => void;\n /** Extra actions rendered alongside the built-in play/add-child/rename/delete/reparent set —\n * placement \"primary\" renders inline on the node (the upload slot from earlier versions used),\n * \"overflow\" (the default) renders in the kebab menu alongside rename/delete. */\n additionalActions?: (node: GenreTreeNode) => GenreTreeAction[];\n /** When true (the default), GenreTree owns its own pan/zoom viewport, zoom buttons, and\n * wheel/drag listeners. Set to false to render just the bare SVG at its natural size with none\n * of that — used by GenreTreeWheel, which applies one shared pan/zoom transform to the tree and\n * its wheel together instead of giving the tree its own independent one. */\n interactive?: boolean;\n /** \"always\" (the default): the viewport captures every wheel event — any wheel/pinch zooms at\n * the cursor, drag pans (Google Maps style) — and all touch input. \"modifier\": only Ctrl/Meta+wheel (or a\n * trackpad pinch) zooms; a plain wheel and one-finger vertical touch scroll the page instead,\n * for trees embedded in a scrolling list. Ignored when `interactive` is false. */\n wheelZoom?: WheelZoomMode;\n /** Stacks the floating zoom/pop/fit controls vertically (\"vertical\", the default) or lays them\n * out in a row (\"horizontal\"). */\n controlsOrientation?: ControlsOrientation;\n /** Host content rendered in the floating controls box, beside the zoom buttons (\"horizontal\")\n * or above them (\"vertical\"); stacked above them in narrow containers. */\n floatingControlsLeading?: ReactNode;\n /** Shows a small spinner beside the zoom buttons (after a short delay, so fast operations don't\n * flash) and marks the tree `aria-busy`. Use it while the host is updating the tree in place. */\n busy?: boolean;\n /** Multiplies the spacing between depth levels along the tree's growth axis. Defaults to 1\n * (no change). */\n depthSpacingScale?: number;\n /** When false, the info panel omits the node actions (play/pause, add-child, rename, reparent,\n * delete, extra actions). Defaults to true. */\n showToolbar?: boolean;\n /** Overrides which node is shown highlighted (with unrelated nodes/links dimmed) instead of\n * GenreTree's own internally-tracked clicked node — used by `interactive={false}` instances\n * (e.g. GenreTreeWheel's nested tree) whose own info panel, and thus own selection, is owned\n * by an ancestor rather than by this instance. On the top-level exported renderers\n * (`GenreTreeWheel`, `GenreTreeWheelRight`, `GenreTreeWheelRadial`,\n * `GenreTreeWheelRadialPopCore`, `GenreTree` itself), setting this also opens and centers the\n * info panel for that node — the same effect a direct click on it would have — swapping in its\n * owning root/sector first if needed. Leave unset to use the internal selection. */\n selectedNodeId?: string | null;\n /** Composes the info panel body for the clicked node: places the built-in `sections` (any\n * omitted one is not rendered) among the consumer's own content — e.g. a consumer-fetched detail\n * like essential tracks. The library has no knowledge of what this renders (loading state, data\n * fetching, etc. are entirely the consumer's responsibility, mirroring `additionalActions`).\n * Omitted, the body is the built-in sections in one column. The panel frame (header, columns,\n * scrolling, edit-mode danger zone) stays the library's. */\n renderInfoPanelBody?: (node: GenreTreeNode, sections: InfoPanelSections) => InfoPanelBody;\n /** Fired when a user action changes the node shown in the info panel (a click on the tree, or on\n * a parent/child/ancestor chip in the panel), or with `null` when the user closes it (its header\n * ✕) — so a consumer driving `selectedNodeId` stays in sync. Not fired when re-selecting the node\n * already shown, when the panel follows the consumer's own `selectedNodeId`, nor when the\n * consumer clears `selectedNodeId` to `null`/`undefined` (which closes the panel). */\n onSelectedNodeChange?: (node: GenreTreeNode | null) => void;\n /** Hides the info panel header's close button — for a consumer that closes the panel itself\n * (by clearing `selectedNodeId`), e.g. from its own search bar. Defaults to false. */\n hideInfoPanelClose?: boolean;\n /** Hides the info panel header's node name — for a consumer that already shows it, e.g. in its own\n * search bar floated over the header (see `--gtv-info-panel-header-inset`). Defaults to false. */\n hideInfoPanelTitle?: boolean;\n /** Keeps `renderDangerZone` inactive. Defaults to false. */\n readOnly?: boolean;\n /** Rendered at the bottom of the info panel unless `readOnly`. */\n renderDangerZone?: (node: GenreTreeNode) => ReactNode;\n /** Whether the info panel fills the tree container (controlled), splitting into a narrow left\n * column (the body's `aside`, then the danger zone) and a wide right one (its `primary`). Selecting\n * another node from the panel keeps it expanded. Defaults to false. */\n expanded?: boolean;\n /** When set, the info panel header shows an expand/collapse toggle firing this with the\n * requested state, and Escape fires it with `false` while expanded (unless focus is in an\n * input/textarea/select or a dialog is open). */\n onExpandedChange?: (on: boolean) => void;\n /** Slides the info panel out of view and makes it inert, keeping it mounted. Defaults to false. */\n collapsed?: boolean;\n /** Drops the header's expand/collapse toggle when the host draws its own control for it. */\n hideExpandButton?: boolean;\n /** Consumer content shown over the tree independent of node selection — at the expanded info\n * panel's size (e.g. an About page), or as an expandable info-panel peer when it sets\n * `onExpandedChange` (e.g. an artist). Its header ✕ and Escape (same exceptions as\n * `onExpandedChange`) fire `onClose`; Escape collapses an expanded panel first. Showing it\n * alongside the info panel is the consumer's call — close one before opening the other to keep\n * them exclusive. */\n overlayPanel?: GenreTreeOverlayPanel;\n /** Seeds the pan/zoom on mount, replacing the initial fit-to-frame and the centering on an\n * initial `selectedNodeId`. Read once; later changes are ignored. Ignored when `interactive` is\n * false and by `GenreTreeOutline`. */\n defaultViewport?: GenreTreeViewport;\n /** Fired once a user-driven pan/zoom settles (drag, wheel, pinch, zoom/fit buttons, centering on\n * a clicked node) — not for the initial fit or for applying `defaultViewport`. */\n onViewportChange?: (viewport: GenreTreeViewport) => void;\n}\n\nexport interface GenreTreeOverlayPanel {\n title: string;\n /** Its top-level children (and `extraContent`'s) are separated by a divider, as is `extraContent`\n * from `content` when stacked — blocks must not draw their own. */\n content: ReactNode;\n onClose: () => void;\n /** With `onExpandedChange`, the panel behaves like the info panel: compact in its left slot,\n * expanding to its expanded size (controlled). Without it, the panel always covers the center\n * column at the expanded size, with no toggle. */\n expanded?: boolean;\n /** Adds the header's expand/collapse toggle; Escape fires it with `false` while expanded. */\n onExpandedChange?: (on: boolean) => void;\n /** Stacked below `content` when compact, the right column when expanded — same as the info\n * panel body's `primary`. */\n extraContent?: ReactNode;\n /** See GenreTreeProps.collapsed. */\n collapsed?: boolean;\n /** See GenreTreeProps.hideExpandButton. */\n hideExpandButton?: boolean;\n /** See GenreTreeProps.hideInfoPanelTitle. */\n hideTitle?: boolean;\n /** See GenreTreeProps.hideInfoPanelClose; Escape still closes. */\n hideClose?: boolean;\n}\n","import * as d3 from \"d3\";\n\nexport function addGrid(\n svg: d3.Selection<SVGGElement, unknown, null, undefined>,\n width: number,\n height: number,\n gridIsHidden: boolean,\n): void {\n const SPACING = 50;\n if (!gridIsHidden) {\n const gridGroup = svg.append(\"g\").attr(\"class\", \"grid\");\n\n for (let y = 0; y <= height; y += SPACING) {\n gridGroup\n .append(\"line\")\n .attr(\"x1\", 0)\n .attr(\"y1\", y)\n .attr(\"x2\", width)\n .attr(\"y2\", y)\n .attr(\"stroke\", \"#ccc\")\n .attr(\"stroke-width\", 1);\n\n gridGroup.append(\"text\").attr(\"x\", 5).attr(\"y\", y - 5).attr(\"fill\", \"#000\").attr(\"font-size\", \"10px\").text(y);\n }\n\n for (let x = 0; x <= width; x += SPACING) {\n gridGroup\n .append(\"line\")\n .attr(\"x1\", x)\n .attr(\"y1\", 0)\n .attr(\"x2\", x)\n .attr(\"y2\", height)\n .attr(\"stroke\", \"#ccc\")\n .attr(\"stroke-width\", 1);\n\n gridGroup.append(\"text\").attr(\"x\", x + 5).attr(\"y\", 15).attr(\"fill\", \"#000\").attr(\"font-size\", \"10px\").text(x);\n }\n }\n}\n","import * as d3 from \"d3\";\n\nimport {\n GenreTreeNode,\n isVerticalOrientation,\n TreeOrientation,\n} from \"../types\";\nimport {\n CONNECTOR_COLOR,\n CONNECTOR_OPACITY,\n calculateLinkWidth,\n calculateNodeDimensions,\n ItemCountRange,\n} from \"../constants\";\n\ntype D3Selection = d3.Selection<SVGGElement, unknown, null, undefined>;\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\ntype D3Link = d3.HierarchyLink<GenreTreeNode>;\n\nexport interface RoundedRectCorners {\n tl: number;\n tr: number;\n br: number;\n bl: number;\n}\n\n// SVG <rect> only takes one uniform rx/ry pair, so a node card needs a <path> instead to square\n// off just its top corners while its hover tab is attached (see tree-renderer.ts's mouseover/\n// mouseleave-timeout handlers), and stay fully rounded otherwise.\nexport function roundedRectPath(\n x: number,\n y: number,\n width: number,\n height: number,\n corners: RoundedRectCorners,\n) {\n const { tl, tr, br, bl } = corners;\n return [\n `M ${x + tl} ${y}`,\n `H ${x + width - tr}`,\n tr ? `A ${tr} ${tr} 0 0 1 ${x + width} ${y + tr}` : \"\",\n `V ${y + height - br}`,\n br ? `A ${br} ${br} 0 0 1 ${x + width - br} ${y + height}` : \"\",\n `H ${x + bl}`,\n bl ? `A ${bl} ${bl} 0 0 1 ${x} ${y + height - bl}` : \"\",\n `V ${y + tl}`,\n tl ? `A ${tl} ${tl} 0 0 1 ${x + tl} ${y}` : \"\",\n \"Z\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\n// Traces only the right, bottom, and left edges of a rect (skipping the top edge entirely,\n// unclosed) so a node card's border can merge with its hover tab above it instead of drawing a\n// visible line across the seam. Assumes the top corners are already square (radius 0) — see the\n// mouseover/mouseleave-timeout handlers in tree-renderer.ts, which only swap to this path once\n// the fill path's top corners are squared too.\nexport function openBottomBorderPath(\n x: number,\n y: number,\n width: number,\n height: number,\n corners: Pick<RoundedRectCorners, \"br\" | \"bl\">,\n) {\n const { br, bl } = corners;\n return [\n `M ${x + width} ${y}`,\n `V ${y + height - br}`,\n br ? `A ${br} ${br} 0 0 1 ${x + width - br} ${y + height}` : \"\",\n `H ${x + bl}`,\n bl ? `A ${bl} ${bl} 0 0 1 ${x} ${y + height - bl}` : \"\",\n `V ${y}`,\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\nexport function appendPaths(\n d3Lib: typeof import(\"d3\"),\n svg: D3Selection,\n treeData: D3Node,\n itemCountRange: ItemCountRange,\n orientation: TreeOrientation = \"horizontal\",\n selectedNodeId: string | null = null,\n) {\n const x = (d: D3Node) =>\n d.x! + calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH / 2;\n const y = (d: D3Node) =>\n d.y! + calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT / 2;\n\n const linkGenerator = isVerticalOrientation(orientation)\n ? d3Lib.linkVertical<D3Link, D3Node>().x(x).y(y)\n : d3Lib.linkHorizontal<D3Link, D3Node>().x(x).y(y);\n\n // A link is \"related\" to the selected node when the selected node is one of its two endpoints\n // (its parent link, or a link to one of its children) — every other link in the tree dims.\n const isRelatedLink = (d: D3Link) =>\n d.source.data.id === selectedNodeId || d.target.data.id === selectedNodeId;\n\n svg\n .selectAll(\"path.gtv-link\")\n .data(treeData.links())\n .enter()\n .append(\"path\")\n .attr(\n \"class\",\n (d) =>\n \"gtv-link\" +\n (selectedNodeId && !isRelatedLink(d) ? \" gtv-link--dimmed\" : \"\"),\n )\n .attr(\"d\", linkGenerator)\n .style(\"fill\", \"none\")\n .style(\"stroke\", CONNECTOR_COLOR)\n .style(\"stroke-width\", (d) =>\n calculateLinkWidth(d.target.data.itemCount, itemCountRange),\n )\n .style(\"stroke-opacity\", CONNECTOR_OPACITY)\n .style(\"stroke-linecap\", \"round\");\n}\n","import * as d3 from \"d3\";\n\nimport {\n depthAxisSign,\n GenreTreeAction,\n GenreTreeNode,\n GenreTreePlayState,\n isVerticalOrientation,\n TreeOrientation,\n} from \"./types\";\nimport {\n HORIZONTAL_SEPARATION_BETWEEN_NODES,\n VERTICAL_SEPARATION_BETWEEN_NODES,\n VERTICAL_ORIENTATION_DEPTH_SEPARATION,\n SIBLING_SEPARATION_BETWEEN_NODES,\n ACTIONS_OVERLAY_WIDTH,\n ACTIONS_OVERLAY_HEIGHT,\n SURFACE_BORDER_COLOR,\n CORNER_RADIUS,\n ELEVATION,\n TEXT_COLOR,\n TEXT_MUTED_COLOR,\n ACCENT_COLOR,\n ACCENT_TEXT_COLOR,\n MAX_NODE_WIDTH,\n MAX_NODE_HEIGHT,\n calculateNodeBorderWidth,\n calculateNodeDimensions,\n calculateNodeFontSize,\n getItemCountRange,\n tintSurface,\n} from \"./constants\";\nimport { addGrid } from \"./d3-helper/d3-grid-helper\";\nimport {\n appendPaths,\n roundedRectPath,\n} from \"./d3-helper/d3-path-helper\";\nimport {\n addReparentTargetOverlay,\n} from \"./NodeHelper\";\n\ntype D3Selection = d3.Selection<SVGGElement, unknown, null, undefined>;\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\n\ninterface SvgDimensions {\n svgWidth: number;\n svgHeight: number;\n highestVerticalCoordinate: number;\n rootDepthOffset: number;\n}\n\nexport interface RenderTreeCallbacks {\n onPlayPause?: (nodeId: string) => void;\n onAddChild?: (parentId: string) => void;\n onRenameRequest?: (node: GenreTreeNode) => void;\n onDeleteRequest?: (node: GenreTreeNode) => void;\n onReparentRequest?: (node: GenreTreeNode) => void;\n onReparentTargetSelect: (newParentId: string) => void;\n onNodeClick?: (node: GenreTreeNode, event: MouseEvent) => void;\n additionalActions?: (node: GenreTreeNode) => GenreTreeAction[];\n playingNodeId?: string | null;\n playState?: GenreTreePlayState;\n}\n\nexport function calculateSvgDimensions(\n d3Lib: typeof import(\"d3\"),\n treeData: D3Node,\n orientation: TreeOrientation = \"horizontal\",\n hideRoot = false,\n depthSpacingScale = 1,\n): SvgDimensions {\n const nodes = treeData.descendants();\n const itemCountRange = getItemCountRange(nodes.map((d) => d.data));\n // The node with the highest itemCount in range always scales to exactly MAX_NODE_*\n // (see calculateNodeDimensions), so the layout budget can use those constants directly.\n const maxNodeDimensions = { WIDTH: MAX_NODE_WIDTH, HEIGHT: MAX_NODE_HEIGHT };\n const maximumLevel = d3Lib.max(nodes, (d) => d.depth)!;\n const depthSeparation =\n VERTICAL_ORIENTATION_DEPTH_SEPARATION * depthSpacingScale;\n const depthSeparationHorizontal =\n HORIZONTAL_SEPARATION_BETWEEN_NODES * depthSpacingScale;\n\n if (isVerticalOrientation(orientation)) {\n // Root sits at the bottom edge (\"vertical\") or top edge (\"vertical-flipped\"), so only that\n // one side needs toolbar/menu clearance — the\n // horizontal branch below reserves the same ACTIONS_OVERLAY_HEIGHT split in half across its\n // two ends (top and bottom margins), so match that per-side amount rather than the full budget.\n // A hidden root renders no card and no toolbar, so it needs neither its own height nor that\n // clearance — the svg's bottom edge instead lands exactly on the root's anchor point.\n const svgHeight = hideRoot\n ? maximumLevel * depthSeparation\n : maximumLevel * depthSeparation +\n maxNodeDimensions.HEIGHT +\n ACTIONS_OVERLAY_HEIGHT / 2;\n\n // Root is centered over its children (Reingold–Tilford), so anchoring the whole svg on the\n // root's own breadth coordinate — rather than the bounding box's midpoint — keeps the root\n // exactly at svgWidth/2. The toolbar only ever renders to a node's right, so only the right\n // half needs its headroom (ACTIONS_OVERLAY_WIDTH); the left half doesn't.\n //\n // Every consumer of a node's x (link endpoints in appendPaths, the rendered card's own\n // center) treats `d.x + own width / 2` as that node's true visual position — d.x alone is\n // just its left edge. The root is no exception (hideRoot only skips drawing its card, not\n // this convention), so it has to be centered on that same point, not on the bare `d.x`.\n // Skipping the `+ width / 2` here left the root's card/chip sitting half its own width left\n // of where its children's links actually converge.\n const rootWidth = calculateNodeDimensions(\n treeData.data.itemCount,\n itemCountRange,\n ).WIDTH;\n const rootAnchorX = treeData.x! + rootWidth / 2;\n\n const leftmostBreadthCoordinate = d3Lib.min(nodes, (d) => d.x)!;\n const rightmostBreadthCoordinate = d3Lib.max(nodes, (d) => d.x)!;\n const leftHalfExtent =\n rootAnchorX - leftmostBreadthCoordinate + maxNodeDimensions.WIDTH / 2;\n const rightHalfExtent =\n rightmostBreadthCoordinate -\n rootAnchorX +\n maxNodeDimensions.WIDTH / 2 +\n ACTIONS_OVERLAY_WIDTH;\n const svgWidth = Math.max(leftHalfExtent, rightHalfExtent) * 2;\n\n // Reuses this slot to carry the root-centering offset applied in setupTreeLayout, rather\n // than a vertical coordinate — the two orientations need different single scalars out of\n // this function and the field isn't worth renaming just for that.\n const rootCenteringOffset = svgWidth / 2 - rootAnchorX;\n\n // \"vertical\": root sits at the depth axis's far end (y = svgHeight); the universal\n // `+ height / 2` rendering convention (see appendPaths/renderTree) then pulls a hidden root's\n // link-anchor point back toward y = svgHeight, i.e. toward the anchor it needs to coincide\n // with — no extra offset needed, same as \"horizontal-anchored-flipped\" below.\n // \"vertical-flipped\": root sits at the depth axis's near end (y = 0) instead, so that same\n // `+ height / 2` convention pushes a hidden root's anchor point *away* from y = 0 by exactly\n // one half-height — the mirror of \"horizontal-anchored\" needing `-rootWidth`, this needs an\n // analogous `-rootHeight` pulled back into the depth axis so the push lands exactly at y = 0.\n const rootHeight = calculateNodeDimensions(\n treeData.data.itemCount,\n itemCountRange,\n ).HEIGHT;\n const rootDepthOffset = hideRoot ? -rootHeight : 0;\n\n return {\n svgWidth,\n svgHeight,\n highestVerticalCoordinate: rootCenteringOffset,\n rootDepthOffset,\n };\n }\n\n if (\n orientation === \"horizontal-anchored\" ||\n orientation === \"horizontal-anchored-flipped\"\n ) {\n const svgWidth =\n maximumLevel * depthSeparationHorizontal +\n maxNodeDimensions.WIDTH +\n ACTIONS_OVERLAY_WIDTH;\n\n // Root is centered over its children (Reingold–Tilford), so anchoring the whole svg on the\n // root's own breadth coordinate keeps the root exactly at svgHeight/2 — same technique as the\n // vertical branch above, but on the Y (breadth) axis since depth grows along X here instead.\n const rootHeight = calculateNodeDimensions(\n treeData.data.itemCount,\n itemCountRange,\n ).HEIGHT;\n const rootAnchorY = treeData.x! + rootHeight / 2;\n\n const topmostBreadthCoordinate = d3Lib.min(nodes, (d) => d.x)!;\n const bottommostBreadthCoordinate = d3Lib.max(nodes, (d) => d.x)!;\n const topHalfExtent =\n rootAnchorY -\n topmostBreadthCoordinate +\n maxNodeDimensions.HEIGHT / 2 +\n ACTIONS_OVERLAY_HEIGHT / 2;\n const bottomHalfExtent =\n bottommostBreadthCoordinate -\n rootAnchorY +\n maxNodeDimensions.HEIGHT / 2 +\n ACTIONS_OVERLAY_HEIGHT / 2;\n const svgHeight = Math.max(topHalfExtent, bottomHalfExtent) * 2;\n\n // Reuses this slot to carry the root-centering offset applied in setupTreeLayout, same as\n // the vertical branch above.\n const rootCenteringOffset = svgHeight / 2 - rootAnchorY;\n\n // A hidden root's own width is otherwise baked into every consumer's `d.x + width / 2`\n // convention (see the comment on the vertical branch above) same as it is for a hidden root's\n // breadth position — except here that convention pushes the root's rendered center *away*\n // from the anchor (depth grows in +x, toward larger x) instead of into it, since the anchor\n // sits at this axis's near/zero end rather than its far end the way the vertical branch's\n // bottom-pinned anchor does. Pulling everything back by the root's own width before that\n // `+ width / 2` is applied lands the root's rendered center exactly one half-width *behind*\n // x=0 — i.e. inside the anchor's chip, matching the vertical branch's root landing inside its\n // chip too. setupTreeLayout applies this shift to every node, not just the root: shifting only\n // the root would leave the root→depth-1 link one rootWidth longer than every later\n // depth-to-depth link, since a per-depth offset cancels out of a distance calculation only\n // when it's the same on both ends.\n const rootWidth = calculateNodeDimensions(\n treeData.data.itemCount,\n itemCountRange,\n ).WIDTH;\n const rootDepthOffset = hideRoot ? -rootWidth : 0;\n\n return {\n svgWidth,\n svgHeight,\n highestVerticalCoordinate: rootCenteringOffset,\n rootDepthOffset,\n };\n }\n\n const highestNodeVerticalCoordinate = d3Lib.min(nodes, (d) => d.x)!;\n const highestVerticalCoordinate =\n highestNodeVerticalCoordinate +\n maxNodeDimensions.HEIGHT / 2 -\n ACTIONS_OVERLAY_HEIGHT / 2;\n const lowestNodeVerticalCoordinate = d3Lib.max(nodes, (d) => d.x)!;\n const lowestVerticalCoordinate =\n lowestNodeVerticalCoordinate +\n maxNodeDimensions.HEIGHT / 2 +\n ACTIONS_OVERLAY_HEIGHT / 2;\n // When every node shares the same breadth coordinate (a single root, or a childless/straight\n // chain), the spread above collapses to just ACTIONS_OVERLAY_HEIGHT, dropping the node's own\n // height from the sum entirely. Floor at a single node's full footprint so the card always fits.\n const svgHeight = Math.max(\n lowestVerticalCoordinate - highestVerticalCoordinate,\n maxNodeDimensions.HEIGHT + ACTIONS_OVERLAY_HEIGHT,\n );\n\n const svgWidth =\n maximumLevel * depthSeparationHorizontal +\n maxNodeDimensions.WIDTH +\n ACTIONS_OVERLAY_WIDTH;\n\n return { svgWidth, svgHeight, highestVerticalCoordinate, rootDepthOffset: 0 };\n}\n\nexport function setupTreeLayout(\n d3Lib: typeof import(\"d3\"),\n treeData: D3Node,\n highestVerticalCoordinate: number,\n orientation: TreeOrientation = \"horizontal\",\n rootDepthOffset = 0,\n svgWidth = 0,\n depthSpacingScale = 1,\n): D3Node {\n if (isVerticalOrientation(orientation)) {\n const maximumLevel = d3Lib.max(treeData.descendants(), (d) => d.depth)!;\n const depthSeparation =\n VERTICAL_ORIENTATION_DEPTH_SEPARATION * depthSpacingScale;\n treeData.each(function (d) {\n d.x = d.x! + highestVerticalCoordinate;\n // \"vertical\": root anchored at the bottom (depthAxisSign -1), depth grows toward y=0.\n // \"vertical-flipped\": root anchored at the top (depthAxisSign +1), depth grows toward\n // maximumLevel * SEPARATION.\n d.y =\n depthAxisSign(orientation) === -1\n ? (maximumLevel - d.depth) * depthSeparation\n : d.depth * depthSeparation + rootDepthOffset;\n });\n return treeData;\n }\n\n if (\n orientation === \"horizontal-anchored\" ||\n orientation === \"horizontal-anchored-flipped\"\n ) {\n // Applied to every node, not just the root: a root-only offset would leave the\n // root→depth-1 gap one rootWidth wider than every later depth-to-depth gap, since it'd\n // shift only one end of that first link. Shifting the whole depth axis instead keeps every\n // gap the uniform HORIZONTAL_SEPARATION_BETWEEN_NODES, while still landing the root's own\n // rendered center on the anchor (see the comment on rootDepthOffset above).\n //\n // \"horizontal-anchored-flipped\" mirrors this around svgWidth instead of 0 — but unlike the\n // non-flipped case, rootDepthOffset must NOT be applied here at all. The `+ width / 2`\n // rendering convention (see renderTree) always shifts a node's center in the +x direction\n // regardless of growth direction, so for the non-flipped anchor (depth grows toward +x) that\n // convention pulls a hidden root's shifted position *back toward* the anchor, and\n // rootDepthOffset must overshoot past it to land half a width behind. For the flipped anchor\n // (depth grows toward -x), that same convention already pushes a hidden root's position\n // *away from* the anchor by exactly one half-width on its own — applying rootDepthOffset on\n // top double-counts that shift and pulls the root a full extra width off the anchor.\n treeData.each(function (d) {\n const tempX = d.x!;\n d.x =\n orientation === \"horizontal-anchored\"\n ? d.y! + rootDepthOffset\n : svgWidth - d.y!;\n d.y = tempX + highestVerticalCoordinate;\n });\n return treeData;\n }\n\n treeData.each(function (d) {\n const tempX = d.x!;\n d.x = d.y!;\n d.y = tempX - highestVerticalCoordinate;\n });\n return treeData;\n}\n\nexport function createTreeLayout(\n d3Lib: typeof import(\"d3\"),\n root: D3Node,\n orientation: TreeOrientation = \"horizontal\",\n depthSpacingScale = 1,\n): D3Node {\n const nodeSize: [number, number] = isVerticalOrientation(orientation)\n ? [\n SIBLING_SEPARATION_BETWEEN_NODES,\n VERTICAL_ORIENTATION_DEPTH_SEPARATION * depthSpacingScale,\n ]\n : [\n VERTICAL_SEPARATION_BETWEEN_NODES,\n HORIZONTAL_SEPARATION_BETWEEN_NODES * depthSpacingScale,\n ];\n // d3's default separation() doubles the gap between same-depth nodes that don't share a\n // parent, which compounds up the tree for deeply-branching data and produces gaps several\n // times wider than the nodeSize slot itself. Every node already reserves its own slot via\n // nodeSize, so a flat 1 keeps that slot's spacing consistent regardless of parentage.\n const treeLayout = d3Lib\n .tree<GenreTreeNode>()\n .nodeSize(nodeSize)\n .separation(() => 1);\n return treeLayout(root);\n}\n\nexport function renderTree(\n d3Lib: typeof import(\"d3\"),\n svgRef: React.RefObject<SVGSVGElement>,\n treeData: D3Node,\n svgWidth: number,\n svgHeight: number,\n reparentingNodeId: string | null,\n reparentForbiddenIds: string[],\n rootColor: string,\n callbacks: RenderTreeCallbacks,\n orientation: TreeOrientation = \"horizontal\",\n hideRoot = false,\n selectedNodeId: string | null = null,\n): D3Selection {\n const { onReparentTargetSelect, onNodeClick } = callbacks;\n\n if (!svgRef.current) {\n throw new Error(\"SVG reference is null\");\n }\n\n const itemCountRange = getItemCountRange(\n treeData.descendants().map((d) => d.data),\n );\n\n const svg = d3Lib\n .select<SVGSVGElement, unknown>(svgRef.current)\n .append(\"svg\")\n .attr(\"width\", svgWidth)\n .attr(\"height\", svgHeight)\n .style(\"overflow\", \"visible\")\n .append(\"g\") as unknown as D3Selection;\n\n // Card elevation shadow, scoped to this root's id so multiple <GenreTree> instances\n // on one page never collide. The id is sanitized since it's embedded into an SVG\n // `id`/`url(#...)` reference, which arbitrary GenreTreeNode.id strings are not safe for.\n const safeRootId = treeData.data.id.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n const shadowFilterId = `gtv-card-shadow-${safeRootId}`;\n if (ELEVATION) {\n const defs = svg.append(\"defs\");\n const filter = defs\n .append(\"filter\")\n .attr(\"id\", shadowFilterId)\n .attr(\"x\", \"-50%\")\n .attr(\"y\", \"-50%\")\n .attr(\"width\", \"200%\")\n .attr(\"height\", \"200%\");\n filter\n .append(\"feDropShadow\")\n .attr(\"dx\", 0)\n .attr(\"dy\", 0)\n .attr(\"stdDeviation\", 1)\n .attr(\"flood-color\", TEXT_COLOR)\n .attr(\"flood-opacity\", 0.12);\n }\n\n addGrid(svg, svgWidth, svgHeight, true);\n\n appendPaths(\n d3Lib,\n svg,\n treeData,\n itemCountRange,\n orientation,\n selectedNodeId,\n );\n\n const isForbidden = (d: D3Node) => reparentForbiddenIds.includes(d.data.id);\n const isSelected = (d: D3Node) => d.data.id === selectedNodeId;\n // A node is \"related\" to the selected node when it's the selected node's own parent or one of\n // its children — these dim less than the rest of the tree.\n const isRelatedNode = (d: D3Node) =>\n d.parent?.data.id === selectedNodeId ||\n (d.children ?? []).some((c) => c.data.id === selectedNodeId);\n\n // With the actual root hidden, the whole visible subtree grows directly out of the root chip —\n // styling every node the same way (solid root color, bold white label) reads as a continuation\n // of that chip instead of a visual gap into plain cards.\n const isSubtreeCore = (d: D3Node) => hideRoot && d.depth >= 1;\n\n // The hidden root still contributes its (x, y) as the anchor endpoint for appendPaths above —\n // only its own card/toolbar is skipped here, not its position.\n const visibleDescendants = hideRoot\n ? treeData.descendants().filter((d) => d.depth !== 0)\n : treeData.descendants();\n\n const nodes = svg\n .selectAll<SVGGElement, D3Node>(\"g.node\")\n .data(visibleDescendants)\n .enter()\n .append(\"g\")\n .attr(\n \"class\",\n (d) =>\n \"node\" +\n (isForbidden(d) ? \" gtv-node--forbidden\" : \"\") +\n (selectedNodeId && !isSelected(d)\n ? isRelatedNode(d)\n ? \" gtv-node--dimmed-related\"\n : \" gtv-node--dimmed\"\n : \"\"),\n )\n .attr(\"id\", (d) => \"group-\" + d.data.id)\n .attr(\"transform\", function (d) {\n const dimensions = calculateNodeDimensions(\n d.data.itemCount,\n itemCountRange,\n );\n const translateX = d.x! + dimensions.WIDTH / 2;\n const translateY = d.y! + dimensions.HEIGHT / 2;\n return `translate(${translateX}, ${translateY})`;\n })\n // Exposed so the toolbar foreignObject (which overlays the card on hover) can mask the\n // label beneath it with the card's own fill instead of a hardcoded color.\n .style(\"--gtv-node-fill\", (d) =>\n isSubtreeCore(d) ? rootColor : tintSurface(rootColor),\n );\n\n // Invisible hit-region spanning the node body, appended before any visible content so painted\n // siblings (rect, label, toolbar) take pointer-event priority over it wherever they overlap\n // it. <g> itself paints nothing, so without this the group's mouseenter/mouseleave below would\n // be governed only by the union of whichever painted children currently exist.\n nodes\n .append(\"rect\")\n .attr(\"class\", \"gtv-hover-hit-area\")\n .attr(\n \"width\",\n (d) => calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH,\n )\n .attr(\n \"height\",\n (d) => calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT,\n )\n .attr(\n \"x\",\n (d) =>\n -calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH / 2,\n )\n .attr(\n \"y\",\n (d) =>\n -calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT / 2,\n )\n .attr(\"fill\", \"transparent\")\n .attr(\"pointer-events\", \"all\");\n\n // Fill and border are separate paths (rather than one path with both fill and stroke) so that\n // hovering a node can drop just its border's top edge (to merge visually with its hover tab\n // above, which already omits its own border-bottom) without needing a stroke API that can\n // fill without stroking. See the mouseover/mouseleave-timeout handlers below.\n //\n // The card path is wrapped in its own <g> so the elevation filter has a stable target: the\n // hover label is appended outside this group (see the mouseover handler below), so mounting or\n // unmounting it on hover never changes what the filter blurs — the card's shadow always looks\n // the same, hovered or not.\n const cardShadowGroups = nodes\n .append(\"g\")\n .attr(\"class\", \"gtv-card-shadow-group\")\n .attr(\"filter\", ELEVATION ? `url(#${shadowFilterId})` : null);\n\n cardShadowGroups\n .append(\"path\")\n .attr(\"class\", \"gtv-node-rect\")\n .attr(\"d\", (d) => {\n const dimensions = calculateNodeDimensions(\n d.data.itemCount,\n itemCountRange,\n );\n return roundedRectPath(\n -dimensions.WIDTH / 2,\n -dimensions.HEIGHT / 2,\n dimensions.WIDTH,\n dimensions.HEIGHT,\n {\n tl: CORNER_RADIUS,\n tr: CORNER_RADIUS,\n br: CORNER_RADIUS,\n bl: CORNER_RADIUS,\n },\n );\n })\n .attr(\"fill\", (d) =>\n isSubtreeCore(d) ? rootColor : tintSurface(rootColor),\n );\n\n nodes\n .append(\"path\")\n .attr(\"class\", \"gtv-node-border\")\n .attr(\"d\", (d) => {\n const dimensions = calculateNodeDimensions(\n d.data.itemCount,\n itemCountRange,\n );\n return roundedRectPath(\n -dimensions.WIDTH / 2,\n -dimensions.HEIGHT / 2,\n dimensions.WIDTH,\n dimensions.HEIGHT,\n {\n tl: CORNER_RADIUS,\n tr: CORNER_RADIUS,\n br: CORNER_RADIUS,\n bl: CORNER_RADIUS,\n },\n );\n })\n .attr(\"fill\", \"none\")\n .attr(\"stroke\", (d) =>\n isSelected(d) ? ACCENT_COLOR : SURFACE_BORDER_COLOR,\n )\n .attr(\"stroke-width\", (d) =>\n calculateNodeBorderWidth(d.data.itemCount, itemCountRange, isSelected(d)),\n );\n\n nodes\n .append(\"foreignObject\")\n .attr(\n \"width\",\n (d) => calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH,\n )\n .attr(\n \"height\",\n (d) => calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT,\n )\n .attr(\n \"x\",\n (d) =>\n -calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH / 2,\n )\n .attr(\n \"y\",\n (d) =>\n -calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT / 2,\n )\n .html((d) => {\n const rootClass =\n (d.depth === 0 || isSubtreeCore(d) ? \" gtv-node-label--root\" : \"\") +\n (d.data.umbrella ? \" gtv-node-label--umbrella\" : \"\");\n const color = isForbidden(d)\n ? TEXT_MUTED_COLOR\n : isSubtreeCore(d)\n ? ACCENT_TEXT_COLOR\n : TEXT_COLOR;\n const fontSize = calculateNodeFontSize(d.data.itemCount, itemCountRange);\n return `<div class=\"gtv-node-label${rootClass}\" style=\"color:${color};font-size:${fontSize}px\">${d.data.name}</div>`;\n });\n\n nodes.each(function (d: D3Node) {\n const group = d3Lib.select<SVGGElement, unknown>(this);\n let leaveTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n group.on(\"mouseenter\", function () {\n // A fast mouse movement between this node's own child shapes (e.g. from the label onto\n // the toolbar) can cross an unpainted gap between them, which the browser reports as\n // briefly leaving and re-entering this <g> (pointer-events only tracks painted areas, and\n // <g> itself paints nothing). Cancel any removal that blip scheduled, or a quick pass-through\n // would still delete the toolbar 100ms later even though the mouse is back on it.\n if (leaveTimeoutId !== null) {\n clearTimeout(leaveTimeoutId);\n leaveTimeoutId = null;\n }\n\n if (reparentingNodeId && d.data.actionable !== false && !isForbidden(d)) {\n addReparentTargetOverlay(\n d3Lib,\n group as unknown as d3.Selection<\n SVGGElement,\n unknown,\n HTMLElement,\n unknown\n >,\n onReparentTargetSelect,\n itemCountRange,\n );\n }\n });\n\n group.on(\"mouseleave\", function () {\n leaveTimeoutId = setTimeout(() => {\n leaveTimeoutId = null;\n d3Lib\n .select<\n SVGGElement,\n unknown\n >(\"#select-as-new-parent-group-\" + d.data.id)\n .remove();\n }, 100);\n });\n\n // Toolbar/menu buttons stopPropagation, so this only fires for a click that actually landed\n // on the card body itself (fill, border, label, or the invisible hit-area). The\n // reparent-target overlay's click doesn't stopPropagation, but it only exists while\n // reparentingNodeId is set, which the guard below already suppresses.\n group.on(\"click\", function (event: MouseEvent) {\n if (reparentingNodeId) return;\n onNodeClick?.(d.data, event);\n });\n });\n\n return svg;\n}\n","import type { ReactNode } from \"react\";\nimport { MdFitScreen, MdZoomIn, MdZoomOut } from \"react-icons/md\";\n\nimport { BusyIndicator } from \"./BusyIndicator\";\nimport type { ControlsOrientation } from \"./types\";\nimport type { UsePanZoomResult } from \"./use-pan-zoom\";\n\ninterface FloatingControlsProps {\n panZoom: Pick<UsePanZoomResult, \"zoomIn\" | \"zoomOut\" | \"canZoomIn\" | \"canZoomOut\">;\n onFit: () => void;\n orientation: ControlsOrientation;\n /** Host content laid out beside (horizontal) or above (vertical) the zoom buttons. */\n leading?: ReactNode;\n busy?: boolean;\n}\n\n/** The bottom-right zoom in / zoom out / fit-to-frame buttons, shared by every tree layout. */\nexport function FloatingControls({ panZoom, onFit, orientation, leading, busy }: FloatingControlsProps) {\n return (\n <div\n className={[\n \"gtv-wheel-floating-controls\",\n orientation === \"horizontal\" && \"gtv-wheel-floating-controls--horizontal\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {busy && <BusyIndicator />}\n {leading && <div className=\"gtv-floating-controls-leading\">{leading}</div>}\n <div className=\"gtv-zoom-controls\">\n <button\n type=\"button\"\n className={[\"gtv-zoom-btn\", !panZoom.canZoomIn && \"gtv-zoom-btn--disabled\"].filter(Boolean).join(\" \")}\n disabled={!panZoom.canZoomIn}\n onClick={panZoom.zoomIn}\n aria-label=\"Zoom in\"\n >\n <MdZoomIn className=\"gtv-icon\" size={18} />\n </button>\n <button\n type=\"button\"\n className={[\"gtv-zoom-btn\", !panZoom.canZoomOut && \"gtv-zoom-btn--disabled\"].filter(Boolean).join(\" \")}\n disabled={!panZoom.canZoomOut}\n onClick={panZoom.zoomOut}\n aria-label=\"Zoom out\"\n >\n <MdZoomOut className=\"gtv-icon\" size={18} />\n </button>\n <button type=\"button\" className=\"gtv-zoom-btn\" onClick={onFit} aria-label=\"Fit to frame\">\n <MdFitScreen className=\"gtv-icon\" size={18} />\n </button>\n </div>\n </div>\n );\n}\n","/** Spinner shown in the floating controls while the host marks the tree `busy`. */\nexport function BusyIndicator() {\n return (\n <div className=\"gtv-busy\" role=\"status\" aria-live=\"polite\">\n <span className=\"gtv-busy-ring\" aria-hidden=\"true\" />\n <span className=\"gtv-skeleton-sr-only\">Updating genre tree…</span>\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n CENTER_ON_ELEMENT_DURATION_MS,\n DEFAULT_VIEWPORT_CENTER_GUARD_MS,\n PAN_MIN_VISIBLE_PX,\n PAN_ZOOM_SETTLE_MS,\n ZOOM_ANIMATION_DURATION_MS,\n ZOOM_FIT_PADDING,\n ZOOM_MAX_SCALE,\n ZOOM_MIN_SCALE,\n ZOOM_PINCH_SCALE_SPEED,\n} from \"./constants\";\nimport {\n classifyWheelEvent,\n clampZoomScale,\n computeFitScale,\n computeZoomScale,\n computeZoomScaleForButton,\n createWheelClassifierState,\n viewToViewport,\n viewportToView,\n} from \"./zoom-pan\";\nimport type { GenreTreeViewport, WheelZoomMode } from \"./types\";\n\nexport interface UsePanZoomOptions {\n defaultViewport?: GenreTreeViewport;\n onViewportChange?: (viewport: GenreTreeViewport) => void;\n}\n\nexport interface UsePanZoomResult {\n panX: number;\n panY: number;\n zoomScale: number;\n /** CSS transform implementing both pan and zoom as one shared coordinate system — apply this\n * to a single \"stage\" element and everything inside it (however many DOM subtrees) moves and\n * scales together with no separate synchronization step. */\n transform: string;\n canZoomIn: boolean;\n canZoomOut: boolean;\n zoomIn: () => void;\n zoomOut: () => void;\n /** Recomputes pan/scale so the union bounding box of the given elements (nulls ignored) fits\n * inside the viewport with ZOOM_FIT_PADDING of clearance. No-ops if none are present/measurable.\n * Also relaxes manual zoom-out's floor to match, when this content needs to go further out\n * than ZOOM_MIN_SCALE — see minScale below. The first successful call applies the hook's\n * `defaultViewport` instead of the fitted pan/scale. `silent` keeps the result from reaching\n * `onViewportChange` — for the automatic fit on mount, as opposed to the fit button. */\n fitToFrame: (elements: (Element | null | undefined)[], options?: { silent?: boolean }) => void;\n /** Animates pan/scale so `element`'s center glides to the center of the viewport's *available*\n * space at `targetScale` (clamped to [minScale, ZOOM_MAX_SCALE]) — used to bring a clicked node\n * to a comfortable reading scale. `targetScale` is a minimum: if the user is already zoomed in\n * past it, the current scale is kept so selecting a node never zooms out. Pass\n * `obscuredWidth`/`obscuredSide` when an overlay (e.g. the info panel) will cover part of the\n * viewport, so the element centers within the space that remains visible beside it rather than\n * the viewport's full width. No-ops if the element/viewport isn't present/measurable. */\n centerOnElement: (\n element: Element | null | undefined,\n targetScale: number,\n obscured?: { width: number; side: \"left\" | \"right\" } | null,\n ) => void;\n handlePointerDown: (event: React.PointerEvent) => void;\n /** CSS `touch-action` for the viewport element — see the `wheelZoom` param. */\n touchAction: \"none\" | \"pan-y\";\n}\n\n/**\n * One pan/zoom stage, anchored to `viewportRef` (the element whose bounding rect defines screen\n * space for cursor-anchored zoom and the zoom buttons' own center). Wheel/pinch zooms at the\n * cursor (Google Maps style); click-and-drag over empty background pans. All of it adjusts `panX`/\n * `panY`/`zoomScale` state directly rather than any ancestor's scroll position, so a consumer\n * applying `transform` to one stage element never needs to keep multiple DOM subtrees in sync.\n *\n * `wheelZoom: \"modifier\"` is for viewports embedded in a scrolling page (e.g. a stacked list of\n * trees): only Ctrl/Meta+wheel (and trackpad pinch, which arrives as ctrlKey wheel) zooms, a\n * vertical-dominant plain wheel is left alone so the page scrolls (a horizontal-dominant one still\n * pans X), one-finger touch scrolls the page vertically (`touchAction: \"pan-y\"`) instead of\n * panning the tree, and a two-finger touch pinch zooms the tree.\n *\n * `stageRef` is the element `transform` is applied to. During a gesture the hook writes the\n * transform to it directly, once per frame, and commits `panX`/`panY`/`zoomScale` state (and so\n * re-renders) only when the gesture settles.\n */\nexport function usePanZoom(\n viewportRef: React.RefObject<HTMLElement | null>,\n wheelZoom: WheelZoomMode = \"always\",\n stageRef: React.RefObject<HTMLElement | null>,\n options: UsePanZoomOptions = {},\n): UsePanZoomResult {\n const [panX, setPanX] = useState(0);\n const [panY, setPanY] = useState(0);\n const [zoomScale, setZoomScale] = useState(1);\n\n // Source of truth for pan/zoom while a gesture is in flight; the state above only catches up in\n // commitView, so a burst of wheel/pointer events costs one DOM write per frame, not one React\n // render per event.\n const viewRef = useRef({ x: 0, y: 0, k: 1 });\n const frameIdRef = useRef<number | null>(null);\n const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const stageAnimationRef = useRef<Animation | null>(null);\n const defaultViewportRef = useRef(options.defaultViewport);\n const centerGuardUntilRef = useRef(0);\n const onViewportChangeRef = useRef(options.onViewportChange);\n useEffect(() => {\n onViewportChangeRef.current = options.onViewportChange;\n }, [options.onViewportChange]);\n\n const writeStage = useCallback(() => {\n const stage = stageRef.current;\n if (!stage) return;\n const { x, y, k } = viewRef.current;\n const transform = `translate(${x}px, ${y}px) scale(${k})`;\n stage.classList.add(\"gtv-stage--moving\");\n // Mid-gesture frames go through a held Web Animation rather than style.transform: Chrome\n // re-layerizes every paint chunk of the stage (~8ms per frame on the full pop/core tree) on an\n // inline transform change, but applies an animation's value on the compositor directly.\n if (typeof stage.animate !== \"function\") {\n stage.style.transform = transform;\n return;\n }\n const keyframes = [{ transform }, { transform }];\n if (stageAnimationRef.current) (stageAnimationRef.current.effect as KeyframeEffect).setKeyframes(keyframes);\n else stageAnimationRef.current = stage.animate(keyframes, { duration: Infinity, fill: \"forwards\" });\n }, [stageRef]);\n\n const commitView = useCallback((notify = true) => {\n if (settleTimerRef.current !== null) {\n clearTimeout(settleTimerRef.current);\n settleTimerRef.current = null;\n }\n if (frameIdRef.current !== null) {\n cancelAnimationFrame(frameIdRef.current);\n frameIdRef.current = null;\n }\n const { x, y, k } = viewRef.current;\n const stage = stageRef.current;\n if (stage) {\n stage.style.transform = `translate(${x}px, ${y}px) scale(${k})`;\n stage.classList.remove(\"gtv-stage--moving\");\n }\n stageAnimationRef.current?.cancel();\n stageAnimationRef.current = null;\n setPanX(x);\n setPanY(y);\n setZoomScale(k);\n const viewport = viewportRef.current;\n if (!notify || !onViewportChangeRef.current || !viewport) return;\n const { width, height } = viewport.getBoundingClientRect();\n if (width > 0 && height > 0) onViewportChangeRef.current(viewToViewport({ x, y, k }, width, height));\n }, [stageRef, viewportRef]);\n\n // Tracks every currently-down pointer by id so a second touch landing mid-drag is recognized as\n // the start of a pinch rather than treated as an unrelated pan. Two-finger touch pinch normally\n // never reaches JS at all (the browser treats it as native page zoom) — that's handled by the\n // viewport's touch-action (see `touchAction`; neither \"none\" nor \"pan-y\" allows pinch-zoom),\n // which routes both touch points here as ordinary pointer events instead. Under \"pan-y\" the\n // touchmove listener below additionally stops the browser from claiming a vertically-drifting\n // pinch as a page scroll.\n const activePointersRef = useRef<Map<number, { x: number; y: number }>>(new Map());\n\n // Called after every viewRef mutation from an input event: coalesces the DOM write into one\n // rAF and (re)arms the idle timer that commits state once input stops.\n const scheduleFrame = useCallback(() => {\n if (frameIdRef.current === null) {\n frameIdRef.current = requestAnimationFrame(() => {\n frameIdRef.current = null;\n writeStage();\n });\n }\n // Drag/pinch commit on pointerup: an idle commit mid-drag (a pause while aiming) would re-render,\n // relayout labels and toggle gtv-stage--moving, restyling every node twice.\n if (activePointersRef.current.size > 0) return;\n if (settleTimerRef.current !== null) clearTimeout(settleTimerRef.current);\n settleTimerRef.current = setTimeout(commitView, PAN_ZOOM_SETTLE_MS);\n }, [writeStage, commitView]);\n\n // Floor for manual zoom-out (wheel/pinch/button), relaxed below ZOOM_MIN_SCALE whenever\n // fitToFrame computes a more permissive scale for the current content — so manual zoom-out can\n // always reach at least as far out as \"fit to frame\" does, instead of bottoming out at the\n // static default while fitToFrame jumps straight past it.\n const [minScale, setMinScale] = useState(ZOOM_MIN_SCALE);\n\n // Content bounding box in local (unscaled, pan-independent) coordinates, captured by the most\n // recent fitToFrame call — lets the pan clamp below convert any future panX/panY/zoomScale into\n // the content's on-screen rect without re-measuring the DOM on every drag/wheel event. Stays\n // null until fitToFrame has run at least once; clampPanAxis treats null as \"nothing to clamp\n // against\" and passes the pan value through unchanged.\n const contentBoundsRef = useRef<{ originX: number; originY: number; width: number; height: number } | null>(null);\n\n // Keeps at least PAN_MIN_VISIBLE_PX of content on-screen along each axis, so a drag or wheel-pan\n // can never carry the tree fully out of view with no visible edge left to drag back from. Axes\n // are independent (an X-axis clamp never depends on the current panY, and vice versa), so each\n // can be clamped separately as its own setState updater runs.\n const clampPanAxis = useCallback((pan: number, scale: number, axis: \"x\" | \"y\") => {\n const viewport = viewportRef.current;\n const bounds = contentBoundsRef.current;\n if (!viewport || !bounds) return pan;\n\n const origin = axis === \"x\" ? bounds.originX : bounds.originY;\n const size = axis === \"x\" ? bounds.width : bounds.height;\n // clientWidth/clientHeight (not getBoundingClientRect) since this runs on every wheel/pointer-\n // move event and only the viewport's own size is needed, not its position — avoids forcing a\n // synchronous layout read in a hot path.\n const viewportSize = axis === \"x\" ? viewport.clientWidth : viewport.clientHeight;\n\n const panMax = viewportSize - PAN_MIN_VISIBLE_PX - origin * scale;\n const panMin = PAN_MIN_VISIBLE_PX - size * scale - origin * scale;\n const lo = Math.min(panMin, panMax);\n const hi = Math.max(panMin, panMax);\n return Math.min(hi, Math.max(lo, pan));\n }, [viewportRef]);\n\n // Reads/writes viewRef.current (not the zoomScale/minScale state closures) so a burst of\n // events firing faster than React can commit a render — a trackpad delivers many wheel ticks\n // per gesture, a touch pinch many pointermoves per frame — each sees the immediately-preceding\n // event's result instead of a stale pre-render scale. Without this, rapid events either collapse\n // onto one stale base (perceived as sluggish/laggy trackpad zoom) or anchor against the wrong\n // scale (perceived as the zoomed point drifting during a fast pinch).\n const zoomAtPoint = useCallback(\n (computeNewScale: (currentScale: number) => number, clientX: number, clientY: number) => {\n const viewport = viewportRef.current;\n if (!viewport) {\n return;\n }\n const view = viewRef.current;\n const currentScale = view.k;\n const newScale = computeNewScale(currentScale);\n if (newScale === currentScale) {\n return;\n }\n\n // A wheel/pinch zoom mid-glide takes over from centerOnElement's animation immediately,\n // rather than fighting it over the same panX/panY/zoomScale state.\n centerAnimationRef.current = null;\n\n const rect = viewport.getBoundingClientRect();\n const contentX = (clientX - rect.left - view.x) / currentScale;\n const contentY = (clientY - rect.top - view.y) / currentScale;\n view.x = clientX - rect.left - contentX * newScale;\n view.y = clientY - rect.top - contentY * newScale;\n view.k = newScale;\n scheduleFrame();\n },\n [viewportRef, scheduleFrame],\n );\n\n // Tracks the ctrl+wheel glide animation started by animateZoomTo below — null when no\n // animation is in flight. `targetScale` (not the already-reached interpolated scale) is what a\n // new wheel event during an in-flight glide extends, so a burst of ticks compounds into one\n // continuous glide-and-settle instead of restarting/jumping on every event, matching Google\n // Maps' feel.\n const zoomAnimationRef = useRef<{\n startScale: number;\n targetScale: number;\n startTime: number;\n clientX: number;\n clientY: number;\n } | null>(null);\n const animationFrameIdRef = useRef<number | null>(null);\n // Running state for classifyWheelEvent (see zoom-pan.ts), distinguishing a physical mouse\n // wheel's few large ticks from a trackpad ctrl+wheel gesture's many small ones so each gets its\n // own response curve.\n const wheelClassifierRef = useRef(createWheelClassifierState());\n\n // stepZoomAnimation schedules itself for the next frame, so it needs to call its own latest\n // version without referencing the `const` it's assigned to inside its own body (disallowed by\n // the hooks linter, mirroring stablePointerMove/handlePointerMoveRef's ref-indirection below).\n const stepZoomAnimationRef = useRef<() => void>(() => {});\n\n const stepZoomAnimation = useCallback(() => {\n const anim = zoomAnimationRef.current;\n const viewport = viewportRef.current;\n if (!anim || !viewport) {\n animationFrameIdRef.current = null;\n return;\n }\n\n const t = Math.min(1, (performance.now() - anim.startTime) / ZOOM_ANIMATION_DURATION_MS);\n const eased = 1 - Math.pow(1 - t, 3);\n const view = viewRef.current;\n const currentScale = view.k;\n const frameScale = anim.startScale + (anim.targetScale - anim.startScale) * eased;\n\n // Re-anchored every frame (not just once at the start) using the current frame's interpolated\n // scale, so the same content point stays under the cursor throughout the glide rather than\n // only at the end.\n const rect = viewport.getBoundingClientRect();\n const contentX = (anim.clientX - rect.left - view.x) / currentScale;\n const contentY = (anim.clientY - rect.top - view.y) / currentScale;\n view.x = anim.clientX - rect.left - contentX * frameScale;\n view.y = anim.clientY - rect.top - contentY * frameScale;\n view.k = frameScale;\n writeStage();\n\n if (t < 1) {\n animationFrameIdRef.current = requestAnimationFrame(() => stepZoomAnimationRef.current());\n } else {\n zoomAnimationRef.current = null;\n animationFrameIdRef.current = null;\n commitView();\n }\n }, [viewportRef, writeStage, commitView]);\n useEffect(() => {\n stepZoomAnimationRef.current = stepZoomAnimation;\n }, [stepZoomAnimation]);\n\n // Entry point for the animated ctrl+wheel zoom path. Retargets an in-flight glide (extending\n // its target and restarting the ease from the current interpolated scale) rather than letting\n // overlapping animations fight, so a fast burst of wheel ticks reads as one continuous glide.\n const animateZoomTo = useCallback(\n (computeNewScale: (currentScale: number) => number, clientX: number, clientY: number) => {\n const inFlight = zoomAnimationRef.current;\n const baseScale = inFlight ? inFlight.targetScale : viewRef.current.k;\n const targetScale = computeNewScale(baseScale);\n if (targetScale === baseScale) {\n return;\n }\n\n // A physical-wheel zoom mid-glide takes over from centerOnElement's animation immediately,\n // rather than fighting it over the same panX/panY/zoomScale state.\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = null;\n\n zoomAnimationRef.current = {\n startScale: viewRef.current.k,\n targetScale,\n startTime: performance.now(),\n clientX,\n clientY,\n };\n\n if (animationFrameIdRef.current === null) {\n animationFrameIdRef.current = requestAnimationFrame(stepZoomAnimation);\n }\n },\n [stepZoomAnimation],\n );\n\n // Keeps the viewport's current center point fixed on screen when its box is resized (matching\n // Google Maps: resizing the map container never re-fits or re-zooms, it just reveals/hides edges\n // around the same center) — panX/panY shift by half the size delta so the point that was in the\n // middle of the old box is still in the middle of the new one, then get re-clamped in case the\n // resize shrank the viewport below what the current pan allows.\n const viewportSizeRef = useRef<{ width: number; height: number } | null>(null);\n useEffect(() => {\n const viewport = viewportRef.current;\n if (!viewport) return;\n\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const { width, height } = entry.contentRect;\n\n const previous = viewportSizeRef.current;\n viewportSizeRef.current = { width, height };\n if (!previous) return;\n\n const dx = (width - previous.width) / 2;\n const dy = (height - previous.height) / 2;\n if (dx === 0 && dy === 0) return;\n\n const view = viewRef.current;\n view.x = clampPanAxis(view.x + dx, view.k, \"x\");\n view.y = clampPanAxis(view.y + dy, view.k, \"y\");\n commitView();\n });\n observer.observe(viewport);\n return () => observer.disconnect();\n }, [viewportRef, clampPanAxis, commitView]);\n\n // Non-passive + attached directly to the DOM node (rather than React's onWheel) because\n // React's wheel handler is passive by default, which silently drops preventDefault() — and\n // without it, ctrl+wheel triggers the browser's own page zoom instead of this one. Registered\n // once (zoomAtPoint/clampPanAxis read live scale via refs rather than being deps here) instead\n // of re-subscribing on every scale change, which previously tore down and rebuilt this listener\n // on every single wheel tick during a continuous trackpad gesture.\n useEffect(() => {\n const viewport = viewportRef.current;\n if (!viewport) {\n return;\n }\n\n const handleWheel = (event: WheelEvent) => {\n // Let the info panel handle its own scroll (it has overflow-y: auto) instead of panning/\n // zooming the tree underneath it — it's rendered as a child of this same viewport, so its\n // wheel events would otherwise bubble up here.\n if ((event.target as Element).closest(\".gtv-info-panel\") !== null) {\n return;\n }\n if (wheelZoom === \"modifier\" && !event.ctrlKey && !event.metaKey) {\n // A horizontal-dominant swipe has no page scroll to yield to — passing it through would\n // leave the tree un-pannable sideways and let macOS turn it into back/forward navigation.\n if (Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return;\n event.preventDefault();\n const view = viewRef.current;\n view.x = clampPanAxis(view.x - event.deltaX, view.k, \"x\");\n scheduleFrame();\n return;\n }\n event.preventDefault();\n const now = performance.now();\n const wheelType = classifyWheelEvent(event.deltaY, now, wheelClassifierRef.current);\n if (wheelType === \"trackpad\") {\n // Trackpad scroll/pinch already arrives as a continuous, high-frequency stream of\n // small deltas — applying it instantly (like Google Maps/MapLibre do) tracks the\n // gesture 1:1 with zero added latency. Easing *each* of these events (as the \"wheel\"\n // branch below does) stacked a ~200ms lag behind every single one, which read as the\n // trackpad path lagging noticeably behind Google Maps' own feel.\n zoomAtPoint((current) => computeZoomScale(current, event.deltaY, minScaleRef.current), event.clientX, event.clientY);\n } else {\n // A physical wheel's notches are few and chunky (one big jump per click), so easing the\n // glide between them reads as smooth rather than laggy.\n animateZoomTo(\n (current) => computeZoomScaleForButton(current, event.deltaY < 0 ? 1 : -1, minScaleRef.current),\n event.clientX,\n event.clientY,\n );\n }\n };\n\n viewport.addEventListener(\"wheel\", handleWheel, { passive: false });\n return () => viewport.removeEventListener(\"wheel\", handleWheel);\n }, [animateZoomTo, zoomAtPoint, viewportRef, clampPanAxis, scheduleFrame, wheelZoom]);\n\n // Under \"pan-y\" the browser may claim a two-finger pinch whose fingers drift vertically as a page\n // scroll (firing pointercancel and killing the pinch). Cancelling touchmove once a second finger\n // is down keeps the gesture ours, while a lone finger still scrolls the page — the same\n // cooperative-gesture split Google Maps uses for embedded maps. Non-passive for the same reason\n // as the wheel listener above.\n useEffect(() => {\n const viewport = viewportRef.current;\n if (!viewport || wheelZoom !== \"modifier\") return;\n const handleTouchMove = (event: TouchEvent) => {\n if (event.touches.length >= 2) event.preventDefault();\n };\n viewport.addEventListener(\"touchmove\", handleTouchMove, { passive: false });\n return () => viewport.removeEventListener(\"touchmove\", handleTouchMove);\n }, [viewportRef, wheelZoom]);\n\n // Fallback for input that never reaches the wheel handler above — e.g. a trackpad/OS/browser\n // combination that doesn't translate a pinch gesture into a ctrlKey wheel event at all.\n // Anchored on the viewport's own center.\n const zoomByButton = useCallback(\n (direction: 1 | -1) => {\n const viewport = viewportRef.current;\n if (!viewport) return;\n const rect = viewport.getBoundingClientRect();\n zoomAtPoint(\n (current) => computeZoomScaleForButton(current, direction, minScaleRef.current),\n rect.left + rect.width / 2,\n rect.top + rect.height / 2,\n );\n commitView();\n },\n [zoomAtPoint, viewportRef, commitView],\n );\n\n // Generalizes zoomAtPoint's screen->content conversion from a single point to the union\n // bounding box of one or more elements, measured live via getBoundingClientRect() — this way\n // fitting the wheel+tree doesn't require re-deriving their geometry (radius, chip offsets,\n // svg dimensions) a second time here.\n const fitToFrame = useCallback(\n (elements: (Element | null | undefined)[], { silent = false }: { silent?: boolean } = {}) => {\n const viewport = viewportRef.current;\n const present = elements.filter((el): el is Element => Boolean(el));\n if (!viewport || present.length === 0) return;\n\n const viewportRect = viewport.getBoundingClientRect();\n if (viewportRect.width <= 0 || viewportRect.height <= 0) return;\n\n // fitToFrame sets panX/panY/zoomScale directly, so any in-flight centerOnElement glide would\n // otherwise resume overwriting it on its very next frame.\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = null;\n // computeFitScale subtracts ZOOM_FIT_PADDING*2 from each dimension and deliberately never\n // clamps its lower bound (see zoom-pan.ts) — a viewport too small to hold the padding alone\n // would drive the result negative, mirroring/flinging the tree out of frame. Bail out rather\n // than fit into a viewport that can't even fit the padding.\n if (viewportRect.width <= ZOOM_FIT_PADDING * 2 || viewportRect.height <= ZOOM_FIT_PADDING * 2) return;\n\n const rects = present.map((el) => el.getBoundingClientRect());\n const contentLeft = Math.min(...rects.map((r) => r.left));\n const contentTop = Math.min(...rects.map((r) => r.top));\n const contentRight = Math.max(...rects.map((r) => r.right));\n const contentBottom = Math.max(...rects.map((r) => r.bottom));\n\n const view = viewRef.current;\n const contentWidth = (contentRight - contentLeft) / view.k;\n const contentHeight = (contentBottom - contentTop) / view.k;\n if (contentWidth <= 0 || contentHeight <= 0) return;\n\n const contentOriginX = (contentLeft - viewportRect.left - view.x) / view.k;\n const contentOriginY = (contentTop - viewportRect.top - view.y) / view.k;\n\n contentBoundsRef.current = {\n originX: contentOriginX,\n originY: contentOriginY,\n width: contentWidth,\n height: contentHeight,\n };\n\n const fitScale = computeFitScale(\n contentWidth,\n contentHeight,\n viewportRect.width,\n viewportRect.height,\n ZOOM_FIT_PADDING,\n );\n\n // Keep viewRef/minScaleRef current immediately so a wheel/pinch event firing right after\n // fitToFrame reads the new scale instead of a stale one from before this render commits.\n const newMinScale = Math.min(ZOOM_MIN_SCALE, fitScale);\n // eslint-disable-next-line react-hooks/immutability -- see comment above\n minScaleRef.current = newMinScale;\n setMinScale(newMinScale);\n const seed = defaultViewportRef.current;\n if (seed) {\n defaultViewportRef.current = undefined;\n const seeded = viewportToView(\n { ...seed, zoom: clampZoomScale(seed.zoom, newMinScale) },\n viewportRect.width,\n viewportRect.height,\n );\n view.k = seeded.k;\n view.x = seeded.x;\n view.y = seeded.y;\n centerGuardUntilRef.current = performance.now() + DEFAULT_VIEWPORT_CENTER_GUARD_MS;\n commitView(false);\n return;\n }\n view.k = fitScale;\n view.x = viewportRect.width / 2 - (contentOriginX + contentWidth / 2) * fitScale;\n view.y = viewportRect.height / 2 - (contentOriginY + contentHeight / 2) * fitScale;\n commitView(!silent);\n },\n [viewportRef, commitView],\n );\n\n // Tracks the eased \"fly to\" glide started by centerOnElement below — unlike zoomAnimationRef's\n // wheel-notch glide (which re-anchors around a fixed *screen* point every frame), this eases\n // panX/panY/zoomScale together toward one fixed target, since the destination here is \"this\n // element's content-space center on screen\", not \"whatever's under the cursor right now\" —\n // matching the single smooth flight of Google Maps' own click-to-center-and-zoom.\n const centerAnimationRef = useRef<{\n startPanX: number;\n startPanY: number;\n startScale: number;\n targetPanX: number;\n targetPanY: number;\n targetScale: number;\n startTime: number;\n } | null>(null);\n const centerAnimationFrameIdRef = useRef<number | null>(null);\n // Ref-indirection for the same reason stepZoomAnimationRef needs it above: this schedules its\n // own next frame and can't reference the `const` it's assigned to from inside its own body.\n const stepCenterAnimationRef = useRef<() => void>(() => {});\n\n const stepCenterAnimation = useCallback(() => {\n const anim = centerAnimationRef.current;\n if (!anim) {\n centerAnimationFrameIdRef.current = null;\n return;\n }\n\n const t = Math.min(1, (performance.now() - anim.startTime) / CENTER_ON_ELEMENT_DURATION_MS);\n const eased = 1 - Math.pow(1 - t, 3);\n const scale = anim.startScale + (anim.targetScale - anim.startScale) * eased;\n const px = anim.startPanX + (anim.targetPanX - anim.startPanX) * eased;\n const py = anim.startPanY + (anim.targetPanY - anim.startPanY) * eased;\n\n const view = viewRef.current;\n view.k = scale;\n view.x = px;\n view.y = py;\n writeStage();\n\n if (t < 1) {\n centerAnimationFrameIdRef.current = requestAnimationFrame(() => stepCenterAnimationRef.current());\n } else {\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = null;\n centerAnimationFrameIdRef.current = null;\n commitView();\n }\n }, [writeStage, commitView]);\n useEffect(() => {\n stepCenterAnimationRef.current = stepCenterAnimation;\n }, [stepCenterAnimation]);\n\n // Same screen->content conversion as fitToFrame/zoomAtPoint, but targeting one element's center\n // at a fixed scale rather than fitting a bounding box — used for \"click a node, bring it to a\n // comfortable reading scale, centered\" instead of \"fit everything on screen\". Glides there via\n // stepCenterAnimation rather than jumping instantly.\n const centerOnElement = useCallback(\n (\n element: Element | null | undefined,\n targetScale: number,\n obscured?: { width: number; side: \"left\" | \"right\" } | null,\n ) => {\n const viewport = viewportRef.current;\n if (!viewport || !element || performance.now() < centerGuardUntilRef.current) return;\n\n const viewportRect = viewport.getBoundingClientRect();\n if (viewportRect.width <= 0 || viewportRect.height <= 0) return;\n\n const rect = element.getBoundingClientRect();\n // Reads viewRef/minScaleRef (not the zoomScale/minScale/panX/panY state closures) for the same reason zoomAtPoint does: this is invoked from a D3 click handler\n // rebound only when a largely-unrelated effect's deps change, so a state closure here would\n // compute against whatever pan/zoom was current when that effect last ran rather than the\n // live value.\n const view = viewRef.current;\n const currentScale = view.k;\n const newScale = clampZoomScale(Math.max(targetScale, currentScale), minScaleRef.current);\n const centerX = (rect.left + rect.width / 2 - viewportRect.left - view.x) / currentScale;\n const centerY = (rect.top + rect.height / 2 - viewportRect.top - view.y) / currentScale;\n // With an overlay obscuring one side, \"centered on screen\" means centered in the strip that\n // remains visible beside it, not the viewport's full width — otherwise the element ends up\n // right where the overlay covers it.\n const availableCenterX = !obscured\n ? viewportRect.width / 2\n : obscured.side === \"left\"\n ? obscured.width + (viewportRect.width - obscured.width) / 2\n : (viewportRect.width - obscured.width) / 2;\n const targetPanX = availableCenterX - centerX * newScale;\n const targetPanY = viewportRect.height / 2 - centerY * newScale;\n\n // Cancel any in-flight wheel-notch glide so it doesn't fight this animation over the same\n // panX/panY/zoomScale state.\n zoomAnimationRef.current = null;\n if (animationFrameIdRef.current !== null) {\n cancelAnimationFrame(animationFrameIdRef.current);\n animationFrameIdRef.current = null;\n }\n\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = {\n startPanX: view.x,\n startPanY: view.y,\n startScale: currentScale,\n targetPanX,\n targetPanY,\n targetScale: newScale,\n startTime: performance.now(),\n };\n if (centerAnimationFrameIdRef.current === null) {\n centerAnimationFrameIdRef.current = requestAnimationFrame(() => stepCenterAnimationRef.current());\n }\n },\n [viewportRef],\n );\n\n // Click-and-drag pan over empty background. Only starts outside a node/its toolbar so it\n // doesn't fight their own click/hover interactions.\n const lastPointRef = useRef({ x: 0, y: 0 });\n\n // handlePointerDown/handlePointerMove/handlePointerUp are registered as window listeners via\n // referentially-stable wrappers (see stablePointerMove/stablePointerUp below), so they can't just\n // read minScale directly — that would freeze it at whatever value was current when the listener\n // was attached. Mirroring it into a ref gives the closures a live read instead; every\n // setMinScale call site updates its ref in the same spot, so there's no separate effect racing\n // those writes.\n const minScaleRef = useRef(minScale);\n\n // Pointers whose pointerdown landed on a node/toolbar/control, or (in \"modifier\" mode) any touch\n // pointer: excluded from single-pointer pan (so they don't fight that element's own click/hover\n // handling, or the page's own vertical scroll) but still tracked so a pinch that starts on top of\n // content — the common case, since the tree fills most of the screen — still works.\n const suppressedPointersRef = useRef<Set<number>>(new Set());\n // Also remembers which pointer ids the current baseline was computed from, so a third finger\n // landing (or the active pair otherwise changing) recomputes it instead of reusing a stale\n // distance/scale from a different pair — which would otherwise cause a sudden jump in zoom.\n const pinchStartRef = useRef<{ ids: [number, number]; distance: number; scale: number } | null>(null);\n\n const pinchPointerIds = useCallback((pointers: Map<number, { x: number; y: number }>): [number, number] => {\n const ids = Array.from(pointers.keys());\n return [ids[0], ids[1]];\n }, []);\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n const pointers = activePointersRef.current;\n if (!pointers.has(event.pointerId)) return;\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });\n\n if (pointers.size >= 2) {\n const [idA, idB] = pinchPointerIds(pointers);\n const a = pointers.get(idA)!;\n const b = pointers.get(idB)!;\n const distance = Math.hypot(a.x - b.x, a.y - b.y);\n const current = pinchStartRef.current;\n if (!current || current.ids[0] !== idA || current.ids[1] !== idB) {\n pinchStartRef.current = { ids: [idA, idB], distance, scale: viewRef.current.k };\n return;\n }\n if (current.distance <= 0) {\n return;\n }\n zoomAtPoint(\n () =>\n clampZoomScale(\n current.scale * Math.pow(distance / current.distance, ZOOM_PINCH_SCALE_SPEED),\n minScaleRef.current,\n ),\n (a.x + b.x) / 2,\n (a.y + b.y) / 2,\n );\n return;\n }\n\n if (suppressedPointersRef.current.has(event.pointerId)) return;\n\n const dx = event.clientX - lastPointRef.current.x;\n const dy = event.clientY - lastPointRef.current.y;\n lastPointRef.current = { x: event.clientX, y: event.clientY };\n const view = viewRef.current;\n view.x = clampPanAxis(view.x + dx, view.k, \"x\");\n view.y = clampPanAxis(view.y + dy, view.k, \"y\");\n scheduleFrame();\n },\n [zoomAtPoint, clampPanAxis, pinchPointerIds, scheduleFrame],\n );\n\n // handlePointerMove/handlePointerUp are recreated whenever zoomAtPoint (and so zoomScale)\n // changes, e.g. mid-pinch — but window.addEventListener/removeEventListener only match by\n // function identity. Registering these referentially stable wrappers instead (which delegate to\n // the latest handler via ref) means the exact same function passed to addEventListener is always\n // the one passed to removeEventListener, so a pinch never leaves a stale listener attached.\n const handlePointerMoveRef = useRef(handlePointerMove);\n useEffect(() => {\n handlePointerMoveRef.current = handlePointerMove;\n }, [handlePointerMove]);\n const handlePointerUpRef = useRef<(event: PointerEvent) => void>(() => {});\n const stablePointerMove = useCallback((event: PointerEvent) => handlePointerMoveRef.current(event), []);\n const stablePointerUp = useCallback((event: PointerEvent) => handlePointerUpRef.current(event), []);\n\n const handlePointerUp = useCallback((event: PointerEvent) => {\n const pointers = activePointersRef.current;\n pointers.delete(event.pointerId);\n suppressedPointersRef.current.delete(event.pointerId);\n\n if (pointers.size < 2) pinchStartRef.current = null;\n const remaining = Array.from(pointers.values());\n // Dropping from two fingers to one resumes as a plain drag, anchored at the finger left on\n // screen, instead of jumping by the distance between the old two-finger midpoint and it.\n if (remaining.length === 1) lastPointRef.current = remaining[0];\n\n if (pointers.size === 0) {\n window.removeEventListener(\"pointermove\", stablePointerMove);\n window.removeEventListener(\"pointerup\", stablePointerUp);\n window.removeEventListener(\"pointercancel\", stablePointerUp);\n commitView();\n }\n }, [stablePointerMove, stablePointerUp, commitView]);\n useEffect(() => {\n handlePointerUpRef.current = handlePointerUp;\n }, [handlePointerUp]);\n\n const handlePointerDown = useCallback((event: React.PointerEvent) => {\n if (event.button !== 0) return;\n // A pointer landing on a node/toolbar/control/info panel is excluded from single-pointer pan (so\n // it doesn't fight that element's own click/hover/text-selection handling) but still tracked below — otherwise a\n // pinch that starts on top of content, the common case since the tree fills most of the screen,\n // would never be recognized as a pinch at all.\n const isInteractive = (event.target as Element).closest(\"g.node, foreignObject, .gtv-zoom-controls, .gtv-wheel-chip, .gtv-info-panel\") !== null;\n // In \"modifier\" mode one finger scrolls the page, so a touch pointer must neither pan (which\n // nudges the tree until the browser fires pointercancel) nor preventDefault the scroll.\n const isPageScrollTouch = wheelZoom === \"modifier\" && event.pointerType === \"touch\";\n\n const pointers = activePointersRef.current;\n const wasEmpty = pointers.size === 0;\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });\n lastPointRef.current = { x: event.clientX, y: event.clientY };\n\n if (isInteractive || isPageScrollTouch) {\n suppressedPointersRef.current.add(event.pointerId);\n } else {\n event.preventDefault();\n // Starting a background drag takes over from any in-flight centerOnElement glide, rather\n // than fighting it over the same panX/panY state.\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = null;\n }\n\n if (wasEmpty) {\n window.addEventListener(\"pointermove\", stablePointerMove);\n window.addEventListener(\"pointerup\", stablePointerUp);\n window.addEventListener(\"pointercancel\", stablePointerUp);\n }\n }, [stablePointerMove, stablePointerUp, wheelZoom]);\n\n // Covers the case handlePointerUp's own cleanup can't: the component unmounting mid-gesture\n // (route change, conditional render) before every pointer has lifted, which would otherwise\n // leave these window listeners attached and still calling setState after unmount.\n useEffect(() => {\n const pointers = activePointersRef.current;\n const suppressed = suppressedPointersRef.current;\n return () => {\n window.removeEventListener(\"pointermove\", stablePointerMove);\n window.removeEventListener(\"pointerup\", stablePointerUp);\n window.removeEventListener(\"pointercancel\", stablePointerUp);\n pointers.clear();\n suppressed.clear();\n pinchStartRef.current = null;\n if (animationFrameIdRef.current !== null) cancelAnimationFrame(animationFrameIdRef.current);\n if (centerAnimationFrameIdRef.current !== null) cancelAnimationFrame(centerAnimationFrameIdRef.current);\n if (frameIdRef.current !== null) cancelAnimationFrame(frameIdRef.current);\n if (settleTimerRef.current !== null) clearTimeout(settleTimerRef.current);\n };\n }, [stablePointerMove, stablePointerUp]);\n\n return {\n panX,\n panY,\n zoomScale,\n transform: `translate(${panX}px, ${panY}px) scale(${zoomScale})`,\n canZoomIn: zoomScale < ZOOM_MAX_SCALE,\n canZoomOut: zoomScale > minScale,\n zoomIn: () => zoomByButton(1),\n zoomOut: () => zoomByButton(-1),\n fitToFrame,\n centerOnElement,\n handlePointerDown,\n touchAction: wheelZoom === \"modifier\" ? \"pan-y\" : \"none\",\n };\n}\n","import {\n ZOOM_BUTTON_SCALE_STEP,\n ZOOM_MAX_SCALE,\n ZOOM_MIN_SCALE,\n ZOOM_WHEEL_MAX_EXPONENT,\n ZOOM_WHEEL_SCALE_SPEED,\n} from \"./constants\";\nimport type { GenreTreeViewport } from \"./types\";\n\n/** `minScale` defaults to ZOOM_MIN_SCALE but accepts an override so manual zoom-out's floor can\n * be relaxed to match a \"fit to frame\" scale computed for content too large for the static\n * default to reach — see usePanZoom's dynamic min-scale tracking. */\nexport function clampZoomScale(scale: number, minScale: number = ZOOM_MIN_SCALE): number {\n return Math.min(ZOOM_MAX_SCALE, Math.max(minScale, scale));\n}\n\nexport interface PanZoomView {\n x: number;\n y: number;\n k: number;\n}\n\nexport function viewToViewport(view: PanZoomView, width: number, height: number): GenreTreeViewport {\n return { x: (width / 2 - view.x) / view.k, y: (height / 2 - view.y) / view.k, zoom: view.k };\n}\n\nexport function viewportToView(viewport: GenreTreeViewport, width: number, height: number): PanZoomView {\n return { x: width / 2 - viewport.x * viewport.zoom, y: height / 2 - viewport.y * viewport.zoom, k: viewport.zoom };\n}\n\n/** Exponential response to wheel delta so the zoom feels proportional at any current scale —\n * a fixed step would feel abrupt when zoomed far out and sluggish when zoomed far in. The exponent\n * is clamped to ±ZOOM_WHEEL_MAX_EXPONENT so an unusually fast swipe (large |wheelDeltaY| in one\n * event) can't produce a runaway multiplicative jump — see that constant's doc comment. */\nexport function computeZoomScale(currentScale: number, wheelDeltaY: number, minScale: number = ZOOM_MIN_SCALE): number {\n const exponent = Math.max(\n -ZOOM_WHEEL_MAX_EXPONENT,\n Math.min(ZOOM_WHEEL_MAX_EXPONENT, -wheelDeltaY * ZOOM_WHEEL_SCALE_SPEED),\n );\n return clampZoomScale(currentScale * Math.exp(exponent), minScale);\n}\n\nexport function computeZoomScaleForButton(currentScale: number, direction: 1 | -1, minScale: number = ZOOM_MIN_SCALE): number {\n return clampZoomScale(currentScale * Math.pow(ZOOM_BUTTON_SCALE_STEP, direction), minScale);\n}\n\n/** Cross-browser quantum a physical mouse wheel's deltaY always lands on exact multiples of\n * (a long-documented Firefox/Chrome quirk); trackpad-generated deltaY values essentially never\n * do. Used by classifyWheelEvent below. */\nconst WHEEL_TICK_DELTA = 4.000244140625;\n\nexport interface WheelClassifierState {\n lastTime: number;\n lastType: \"wheel\" | \"trackpad\" | null;\n /** True after a gesture's first ambiguous event, whose timeDelta is the gap since the previous\n * gesture rather than a real event rate — the gesture's second event re-decides from its rate. */\n provisional: boolean;\n}\n\nexport function createWheelClassifierState(): WheelClassifierState {\n return { lastTime: 0, lastType: null, provisional: false };\n}\n\n/** Classifies a ctrl+wheel event as a physical mouse wheel (few large, quantized deltaY steps) or\n * a trackpad ctrl+wheel/pinch gesture (many small deltaY events) — adapted from MapLibre/Mapbox\n * GL's scroll-zoom heuristic, so each can get its own response curve instead of sharing one.\n * `state` carries the running classification across a gesture's repeated events and is mutated\n * in place. */\nexport function classifyWheelEvent(deltaY: number, now: number, state: WheelClassifierState): \"wheel\" | \"trackpad\" {\n const timeDelta = now - state.lastTime;\n state.lastTime = now;\n\n let type: \"wheel\" | \"trackpad\";\n if (deltaY !== 0 && deltaY % WHEEL_TICK_DELTA === 0) {\n // Definitely a mouse wheel: this exact quantization essentially never occurs by chance.\n type = \"wheel\";\n } else if (Math.abs(deltaY) < 4) {\n // Definitely a trackpad: too small to be a real wheel notch.\n type = \"trackpad\";\n } else if (timeDelta > 400 || !state.lastType || state.provisional) {\n // Infer from delta-per-time — a fast trackpad still moves less per ms than a wheel notch does.\n // A new gesture's first event only gets a provisional guess (see `provisional`), as in\n // MapLibre, which otherwise labels nearly every trackpad scroll \"wheel\" from its first event.\n type = Math.abs(timeDelta * deltaY) < 200 ? \"trackpad\" : \"wheel\";\n state.provisional = timeDelta > 400 || !state.lastType;\n } else {\n // Ambiguous repeat within the same gesture: stay with whatever this gesture already is.\n type = state.lastType;\n }\n\n state.lastType = type;\n return type;\n}\n\n/** Selector for the elements \"fit to frame\" should measure: the tree's actually-rendered cards\n * (or radial dots and their labels) and connector links. Deliberately excludes the invisible per-node hit area and the SVG's own\n * declared width/height, both of which bake in ACTIONS_OVERLAY_WIDTH/HEIGHT (see constants.ts) —\n * toolbar/menu clearance reserved to one side of the tree. Fitting to that reserved space instead\n * of the visible content leaves a large, lopsided gap around the tree after fitting. */\nexport const TREE_CONTENT_SELECTOR = \".gtv-node-rect, .gtv-node-dot, text.gtv-node-label, .gtv-link\";\n\n/** Collects the tight-content elements (see TREE_CONTENT_SELECTOR) under `container`, ready to\n * pass into usePanZoom's fitToFrame. */\nexport function queryTreeContentElements(container: Element | null | undefined): Element[] {\n if (!container) return [];\n return Array.from(container.querySelectorAll(TREE_CONTENT_SELECTOR));\n}\n\n/** Scale that fits `contentWidth`×`contentHeight` inside `viewportWidth`×`viewportHeight` with\n * `padding` px of clearance on every side, picking whichever dimension is more constraining.\n * Capped at 1 (never zooms in past the tree's natural size), not just `ZOOM_MAX_SCALE`: content is\n * measured from `TREE_CONTENT_SELECTOR` alone, excluding the ACTIONS_OVERLAY_WIDTH/HEIGHT space\n * the layout reserves for toolbars/menus. That reserved space scales together with the tree\n * under the shared zoom transform, so scaling past 1 would inflate it past the fixed-px `padding`\n * and clip an edge node's toolbar/menu against the viewport. At scale ≤ 1 the layout's own reserved\n * space is never exceeded, so clipping can't happen. This deliberately ignores `ZOOM_MIN_SCALE`:\n * capping it there would make the button lie about \"fitting\" content too large to fit at that\n * static floor, rendering it too big for the viewport while still centering on the full (larger)\n * bounding box, cropping content. usePanZoom instead relaxes its own interactive floor to match,\n * so manual zoom-out can always reach at least as far out as this computes. */\nexport function computeFitScale(\n contentWidth: number,\n contentHeight: number,\n viewportWidth: number,\n viewportHeight: number,\n padding: number,\n): number {\n return Math.min(\n 1,\n ZOOM_MAX_SCALE,\n (viewportWidth - padding * 2) / contentWidth,\n (viewportHeight - padding * 2) / contentHeight,\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { GenreTreeNode } from \"./types\";\nimport { resolveInfoPanelSide } from \"./info-panel-geometry\";\n\nexport interface NodeInfoPanelState {\n node: GenreTreeNode;\n side: \"left\";\n}\n\nexport interface UseNodeInfoPanelOptions {\n /** The consumer-controlled selection (see GenreTreeProps.selectedNodeId). A transition from an\n * id to `null`/`undefined` closes the panel without firing `onSelectedNodeChange`, since the\n * consumer initiated it. */\n selectedNodeId?: string | null;\n /** See GenreTreeProps.onSelectedNodeChange. */\n onSelectedNodeChange?: (node: GenreTreeNode | null) => void;\n}\n\nexport interface UseNodeInfoPanelResult {\n panel: NodeInfoPanelState | null;\n /** Opens (or updates, if already open) the panel for `node`. No-ops if `element` or `viewport`\n * is missing (e.g. not yet mounted in jsdom). Notifies `onSelectedNodeChange` when the shown node\n * changes, unless `notify` is false — pass that when syncing to the consumer's `selectedNodeId`. */\n showNodeInfo: (\n node: GenreTreeNode,\n element: Element | null | undefined,\n viewport: Element | null | undefined,\n options?: { notify?: boolean },\n ) => void;\n closeNodeInfo: () => void;\n}\n\n/** Owns the \"which node's info panel is open, and on which side\" state for one renderer instance\n * (see GenreTree.tsx and the wheel renderers' top-level components) — not global, not lifted to\n * the consumer, mirroring how usePanZoom is instantiated once per viewport. Every open/close goes\n * through here, so this is the one place that notifies the consumer of selection changes. */\nexport function useNodeInfoPanel({\n selectedNodeId,\n onSelectedNodeChange,\n}: UseNodeInfoPanelOptions = {}): UseNodeInfoPanelResult {\n const [panel, setPanel] = useState<NodeInfoPanelState | null>(null);\n const [prevSelectedNodeId, setPrevSelectedNodeId] = useState(selectedNodeId);\n if (selectedNodeId !== prevSelectedNodeId) {\n setPrevSelectedNodeId(selectedNodeId);\n if (prevSelectedNodeId != null && selectedNodeId == null) setPanel(null);\n }\n\n // Also written synchronously in show/close so two calls in one tick compare against each other.\n const shownNodeIdRef = useRef<string | null>(null);\n const onSelectedNodeChangeRef = useRef(onSelectedNodeChange);\n useEffect(() => {\n shownNodeIdRef.current = panel?.node.id ?? null;\n onSelectedNodeChangeRef.current = onSelectedNodeChange;\n });\n\n const showNodeInfo = useCallback(\n (\n node: GenreTreeNode,\n element: Element | null | undefined,\n viewport: Element | null | undefined,\n { notify = true }: { notify?: boolean } = {},\n ) => {\n if (!element || !viewport) return;\n setPanel({ node, side: resolveInfoPanelSide() });\n const changed = node.id !== shownNodeIdRef.current;\n shownNodeIdRef.current = node.id;\n if (changed && notify) onSelectedNodeChangeRef.current?.(node);\n },\n [],\n );\n\n const closeNodeInfo = useCallback(() => {\n setPanel(null);\n shownNodeIdRef.current = null;\n onSelectedNodeChangeRef.current?.(null);\n }, []);\n\n return { panel, showNodeInfo, closeNodeInfo };\n}\n","/** The info panel always renders on the left side of the viewport. */\nexport function resolveInfoPanelSide(): \"left\" {\n return \"left\";\n}\n\n/** Packages the panel's side as the `centerOnElement` \"obscured\" argument (use-pan-zoom.ts) so a\n * node click can center the node within the space that remains visible once the panel covers the\n * left side. */\nexport function resolveInfoPanelObscuredArea(\n element: Element | null | undefined,\n viewport: Element | null | undefined,\n panelWidth: number,\n): { width: number; side: \"left\" } | null {\n if (!element || !viewport) return null;\n return { width: panelWidth, side: resolveInfoPanelSide() };\n}\n","import { useMemo } from \"react\";\n\nimport { buildTreeIndex, TreeIndex } from \"./tree-index\";\nimport { GenreTreeNode } from \"./types\";\n\n/** `buildTreeIndex(nodes)`, rebuilt only when `nodes`' identity changes. */\nexport function useTreeIndex(nodes: readonly GenreTreeNode[]): TreeIndex {\n return useMemo(() => buildTreeIndex(nodes), [nodes]);\n}\n","import { GenreTreeNode } from \"./types\";\n\nexport interface TreeIndex {\n byId: ReadonlyMap<string, GenreTreeNode>;\n /** Direct children per parent id, in `nodes` order. */\n childrenByParentId: ReadonlyMap<string, readonly GenreTreeNode[]>;\n /** Each node's top-level ancestor id (a node with `parentId === null`), or `null` when its\n * `parentId` chain terminates on an id absent from `nodes` (dangling reference). */\n rootIdById: ReadonlyMap<string, string | null>;\n /** `id`'s ancestors, root-first, excluding `id` itself — stops at a dangling `parentId`. Empty\n * for a root or an unknown id. */\n ancestorsOf: (id: string) => GenreTreeNode[];\n}\n\n/** One O(n) pass over `nodes` building every lookup the renderers need, so per-node queries\n * (root id, children, ancestors, by-id) stay O(1)/O(depth) instead of rescanning `nodes`. */\nexport function buildTreeIndex(nodes: readonly GenreTreeNode[]): TreeIndex {\n const byId = new Map(nodes.map((node) => [node.id, node]));\n\n const childrenByParentId = new Map<string, GenreTreeNode[]>();\n for (const node of nodes) {\n if (node.parentId === null) continue;\n const siblings = childrenByParentId.get(node.parentId);\n if (siblings) siblings.push(node);\n else childrenByParentId.set(node.parentId, [node]);\n }\n\n const rootIdById = new Map<string, string | null>();\n const rootIdOf = (id: string): string | null => {\n if (rootIdById.has(id)) return rootIdById.get(id)!;\n const node = byId.get(id);\n if (!node) return null;\n const rootId = node.parentId === null ? node.id : rootIdOf(node.parentId);\n rootIdById.set(id, rootId);\n return rootId;\n };\n for (const node of nodes) rootIdOf(node.id);\n\n const ancestorsOf = (id: string): GenreTreeNode[] => {\n const ancestors: GenreTreeNode[] = [];\n let parentId = byId.get(id)?.parentId ?? null;\n while (parentId !== null) {\n const parent = byId.get(parentId);\n if (!parent) break;\n ancestors.push(parent);\n parentId = parent.parentId;\n }\n return ancestors.reverse();\n };\n\n return { byId, childrenByParentId, rootIdById, ancestorsOf };\n}\n","import { ReactNode, useEffect, useRef } from \"react\";\nimport { MdArrowBack, MdClose, MdZoomIn } from \"react-icons/md\";\n\nimport { getItemCountRange, ItemCountRange, logarithmicPosition } from \"./constants\";\nimport { InfoPanelExpandButton } from \"./InfoPanelExpandButton\";\nimport { InfoPanelSection } from \"./InfoPanelSection\";\nimport { NodeToolbar, NodeToolbarProps } from \"./NodeToolbar\";\nimport { GenreTreeNode, InfoPanelBody, InfoPanelSections } from \"./types\";\n\n// Same log-scaled position as a tree card (scaled across the mini tree's own nodes), mapped onto\n// a range that stays legible in the 25.5rem panel instead of the tree's 35-120px card heights.\nconst MINI_NODE_MIN_DIAMETER = 14;\nconst MINI_NODE_MAX_DIAMETER = 32;\nconst MINI_NODE_MIN_FONT_SIZE = 10;\nconst MINI_NODE_MAX_FONT_SIZE = 14;\n\n/** A direct child of the panel's node, styled to match exactly how that child renders as its own\n * `gtv-node-rect` out in the tree (solid vs. tinted fill, light vs. dark text) — computed by the\n * caller, since that styling depends on renderer-specific state (e.g. `hideRoot`, pop/core sector)\n * this component has no visibility into. */\nexport interface InfoPanelChild {\n node: GenreTreeNode;\n fill: string;\n textColor: string;\n}\n\nexport interface InfoPanelProps {\n node: GenreTreeNode;\n /** Fill and text color matching how `node` itself renders as its own `gtv-node-rect` out in the\n * tree — computed by the caller for the same reason as `InfoPanelChild`'s (see above). */\n fill: string;\n textColor: string;\n /** `node`'s own parent, styled the same way as `InfoPanelChild` — null for a root, which has\n * none. */\n parentNode: InfoPanelChild | null;\n childNodes: InfoPanelChild[];\n /** `node`'s ancestors root-first, including its immediate parent. Empty for a root. */\n ancestorNodes: InfoPanelChild[];\n side: \"left\" | \"right\";\n onClose: () => void;\n /** Fired when the parent chip or a child chip is clicked, with that node's id — the caller\n * navigates the panel (and the tree's own selection/centering) to it. */\n onSelectNode: (nodeId: string) => void;\n /** Reselects the previously selected node; the button is hidden when omitted. */\n onPrevious?: () => void;\n previousLabel?: string;\n /** See GenreTreeProps.hideInfoPanelClose. */\n hideClose?: boolean;\n /** See GenreTreeProps.hideInfoPanelTitle. */\n hideTitle?: boolean;\n /** See GenreTreeProps.itemCountLabel. */\n itemCountLabel: string;\n /** See GenreTreeProps.renderInfoPanelBody. */\n renderInfoPanelBody?: (node: GenreTreeNode, sections: InfoPanelSections) => InfoPanelBody;\n /** When set, shows a \"Zoom in\" action that fires it. */\n onZoomIn?: () => void;\n /** When set, renders the node's play/add-child/rename/reparent/delete actions below the\n * header. Omitted when the toolbar is disabled (see GenreTreeProps.showToolbar). */\n actions?: Omit<NodeToolbarProps, \"node\" | \"className\">;\n /** See GenreTreeProps.readOnly. */\n readOnly?: boolean;\n /** See GenreTreeProps.renderDangerZone. */\n renderDangerZone?: (node: GenreTreeNode) => ReactNode;\n /** See GenreTreeProps.expanded. */\n expanded?: boolean;\n /** See GenreTreeProps.onExpandedChange. */\n onExpandedChange?: (on: boolean) => void;\n /** See GenreTreeProps.collapsed. */\n collapsed?: boolean;\n /** See GenreTreeProps.hideExpandButton. */\n hideExpandButton?: boolean;\n}\n\nexport function isEscapeOwnedElsewhere(event: KeyboardEvent): boolean {\n if (event.defaultPrevented) return true;\n const target = event.target;\n if (target instanceof Element && target.closest(\"input, textarea, select, [contenteditable]:not([contenteditable=false])\")) {\n return true;\n }\n return document.querySelector('dialog[open], [role=\"dialog\"][aria-modal=\"true\"], [role=\"alertdialog\"]') !== null;\n}\n\n/** Displays a clicked node's own fields, its parent, and its direct children — no fetching, no\n * mutation, purely presentational (see ARCHITECTURE.md). Anchored to `side`\n * (see use-node-info-panel.ts/info-panel-geometry.ts for how that's decided) by the caller's own\n * layout, not by this component. */\nexport function InfoPanel({\n node,\n fill,\n textColor,\n parentNode,\n childNodes,\n ancestorNodes,\n side,\n onClose,\n onSelectNode,\n onPrevious,\n previousLabel,\n renderInfoPanelBody,\n hideClose = false,\n hideTitle = false,\n itemCountLabel,\n onZoomIn,\n actions,\n readOnly = false,\n renderDangerZone,\n expanded = false,\n onExpandedChange,\n collapsed = false,\n hideExpandButton = false,\n}: InfoPanelProps) {\n const panelRef = useRef<HTMLDivElement>(null);\n // `inert` is a boolean attribute in React 19 but an unknown string one in 18 (the peer range covers both).\n useEffect(() => {\n panelRef.current?.toggleAttribute(\"inert\", collapsed);\n }, [collapsed]);\n const miniTreeRange = getItemCountRange([\n ...(parentNode ? [parentNode.node] : []),\n node,\n ...childNodes.map((child) => child.node),\n ]);\n const ancestorRange = getItemCountRange(ancestorNodes.map((ancestor) => ancestor.node));\n const miniNodeSize = ({ itemCount }: GenreTreeNode, range: ItemCountRange) => {\n const t = logarithmicPosition(itemCount, range);\n return {\n diameter: MINI_NODE_MIN_DIAMETER + t * (MINI_NODE_MAX_DIAMETER - MINI_NODE_MIN_DIAMETER),\n fontSize: MINI_NODE_MIN_FONT_SIZE + t * (MINI_NODE_MAX_FONT_SIZE - MINI_NODE_MIN_FONT_SIZE),\n };\n };\n const miniNodeContent = (target: GenreTreeNode, background: string, range: ItemCountRange) => {\n const { diameter, fontSize } = miniNodeSize(target, range);\n return (\n <>\n <span className=\"gtv-info-panel-mini-dot\" style={{ background, width: diameter, height: diameter }} />\n <span className=\"gtv-info-panel-mini-label\" style={{ fontSize }}>\n {target.name}\n </span>\n </>\n );\n };\n const sortedChildNodes = [...childNodes].sort((a, b) => b.node.itemCount - a.node.itemCount);\n\n useEffect(() => {\n if (!expanded || !onExpandedChange) return;\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\" || isEscapeOwnedElsewhere(event)) return;\n onExpandedChange(false);\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [expanded, onExpandedChange]);\n\n const dangerZone = !readOnly && renderDangerZone && (\n <div className=\"gtv-info-panel-danger-zone\">{renderDangerZone(node)}</div>\n );\n\n const toolbar = actions ? <NodeToolbar key={node.id} node={node} className=\"gtv-info-panel-toolbar\" {...actions} /> : null;\n const ancestors =\n ancestorNodes.length > 0 ? (\n <InfoPanelSection title=\"Ancestors\">\n <ul className=\"gtv-info-panel-children-list gtv-info-panel-ancestors-list\">\n {ancestorNodes.map(({ node: ancestor, fill }) => (\n <li key={ancestor.id}>\n <button\n type=\"button\"\n className=\"gtv-info-panel-child\"\n title={ancestor.name}\n onClick={() => onSelectNode(ancestor.id)}\n >\n {miniNodeContent(ancestor, fill, ancestorRange)}\n </button>\n </li>\n ))}\n </ul>\n </InfoPanelSection>\n ) : null;\n const family = (\n <>\n <InfoPanelSection title=\"Family\">\n <div className=\"gtv-info-panel-mini-tree\">\n {parentNode && (\n <div className=\"gtv-info-panel-mini-tree-parent\">\n <button\n type=\"button\"\n className=\"gtv-info-panel-child\"\n title={parentNode.node.name}\n onClick={() => onSelectNode(parentNode.node.id)}\n >\n {miniNodeContent(parentNode.node, parentNode.fill, miniTreeRange)}\n </button>\n </div>\n )}\n <div\n className={`gtv-info-panel-mini-tree-node${childNodes.length === 0 ? \" gtv-info-panel-mini-tree-node--leaf\" : \"\"}`}\n >\n <span\n className=\"gtv-info-panel-child gtv-info-panel-mini-tree-current\"\n title={node.name}\n >\n {miniNodeContent(node, fill, miniTreeRange)}\n </span>\n </div>\n <div className=\"gtv-info-panel-mini-tree-children\">\n <span className=\"gtv-info-panel-children-title\">{childNodes.length} {childNodes.length <= 1 ? \"child\" : \"children\"}</span>\n {childNodes.length > 0 && (\n <ul className=\"gtv-info-panel-children-list\">\n {sortedChildNodes.map(({ node: child, fill }) => (\n <li key={child.id}>\n <button\n type=\"button\"\n className=\"gtv-info-panel-child\"\n title={child.name}\n onClick={() => onSelectNode(child.id)}\n >\n {miniNodeContent(child, fill, miniTreeRange)}\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n </InfoPanelSection>\n </>\n );\n const itemCount = (\n <dl className=\"gtv-info-panel-fields\">\n <dt>{itemCountLabel}</dt>\n <dd>{node.itemCount}</dd>\n </dl>\n );\n const { primary, aside } = renderInfoPanelBody\n ? renderInfoPanelBody(node, { toolbar, ancestors, family, itemCount })\n : { primary: <>{toolbar}{ancestors}{family}{itemCount}</> };\n const hasAside = aside !== undefined && aside !== null && aside !== false;\n\n return (\n <div\n className={`gtv-info-panel gtv-info-panel--${side}${expanded ? \" gtv-info-panel--expanded\" : \"\"}${expanded && !hasAside ? \" gtv-info-panel--expanded-solo\" : \"\"}${collapsed ? \" gtv-info-panel--collapsed\" : \"\"}`}\n ref={panelRef}\n >\n <div\n className=\"gtv-info-panel-header\"\n style={{ background: fill, color: textColor }}\n >\n {onPrevious && (\n <button\n type=\"button\"\n className=\"gtv-info-panel-previous\"\n style={{ color: textColor }}\n onClick={onPrevious}\n aria-label={previousLabel ? `Previous: ${previousLabel}` : \"Previous\"}\n title={previousLabel}\n >\n <MdArrowBack className=\"gtv-icon\" size={16} />\n </button>\n )}\n <span\n className={`gtv-info-panel-title${hideTitle ? \" gtv-info-panel-title--hidden\" : \"\"}`}\n style={{ color: textColor }}\n >\n {node.name}\n {node.umbrella && <span className=\"gtv-info-panel-umbrella-badge\">umbrella</span>}\n </span>\n {onZoomIn && (\n <button\n type=\"button\"\n className=\"gtv-info-panel-zoom-in\"\n style={{ color: textColor }}\n onClick={onZoomIn}\n aria-label=\"Zoom in\"\n title=\"Zoom in\"\n >\n <MdZoomIn className=\"gtv-icon\" size={16} />\n </button>\n )}\n {onExpandedChange && !hideExpandButton && (\n <InfoPanelExpandButton expanded={expanded} onExpandedChange={onExpandedChange} style={{ color: textColor }} />\n )}\n {!hideClose && (\n <button\n type=\"button\"\n className=\"gtv-info-panel-close\"\n style={{ color: textColor }}\n onClick={onClose}\n aria-label=\"Close\"\n >\n <MdClose className=\"gtv-icon\" size={16} />\n </button>\n )}\n </div>\n <div className=\"gtv-info-panel-body\">\n <div className=\"gtv-info-panel-primary\">\n {primary}\n {expanded && !hasAside && dangerZone}\n </div>\n {hasAside && (\n <div className=\"gtv-info-panel-aside\">\n {aside}\n {expanded && dangerZone}\n </div>\n )}\n </div>\n {!expanded && dangerZone}\n </div>\n );\n}\n","import { CSSProperties } from \"react\";\nimport { MdCloseFullscreen, MdOpenInFull } from \"react-icons/md\";\n\ninterface InfoPanelExpandButtonProps {\n expanded: boolean;\n onExpandedChange: (on: boolean) => void;\n style?: CSSProperties;\n}\n\nexport function InfoPanelExpandButton({ expanded, onExpandedChange, style }: InfoPanelExpandButtonProps) {\n return (\n <button\n type=\"button\"\n className=\"gtv-info-panel-close\"\n style={style}\n onClick={() => onExpandedChange(!expanded)}\n aria-label={expanded ? \"Collapse panel\" : \"Expand panel\"}\n >\n {expanded ? <MdCloseFullscreen className=\"gtv-icon\" size={16} /> : <MdOpenInFull className=\"gtv-icon\" size={16} />}\n </button>\n );\n}\n","import { ReactNode, useState } from \"react\";\nimport { MdExpandMore } from \"react-icons/md\";\n\ninterface InfoPanelSectionProps {\n title: string;\n children: ReactNode;\n}\n\n/** A titled InfoPanel section whose body collapses/expands from a chevron toggle in its header. */\nexport function InfoPanelSection({ title, children }: InfoPanelSectionProps) {\n const [open, setOpen] = useState(true);\n return (\n <div className=\"gtv-info-panel-children\">\n <button\n type=\"button\"\n className=\"gtv-info-panel-section-toggle\"\n aria-expanded={open}\n onClick={() => setOpen((isOpen) => !isOpen)}\n >\n <span className=\"gtv-info-panel-children-title\">{title}</span>\n <MdExpandMore className=\"gtv-icon gtv-info-panel-section-chevron\" size={16} />\n </button>\n {open && children}\n </div>\n );\n}\n","\"use client\";\n\nimport { useState } from \"react\";\nimport { MdMoreVert, MdModeEdit } from \"react-icons/md\";\nimport { FaPlus, FaTrashAlt, FaPlay, FaPause, FaSpinner } from \"react-icons/fa\";\nimport { PiGraphFill } from \"react-icons/pi\";\n\nimport { GenreTreeAction, GenreTreeNode, GenreTreePlayState } from \"./types\";\n\nexport interface NodeToolbarProps {\n node: GenreTreeNode;\n playingNodeId?: string | null;\n playState?: GenreTreePlayState;\n onPlayPause?: (nodeId: string) => void;\n onAddChild?: (parentId: string) => void;\n onRenameRequest?: (node: GenreTreeNode) => void;\n onDeleteRequest?: (node: GenreTreeNode) => void;\n onReparentRequest?: (node: GenreTreeNode) => void;\n additionalActions?: (node: GenreTreeNode) => GenreTreeAction[];\n className?: string;\n}\n\n/** The play/add-child/rename/reparent/delete/extra actions for a node, rendered in the info panel. */\nexport function NodeToolbar({\n node,\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n className,\n}: NodeToolbarProps) {\n const [menuOpen, setMenuOpen] = useState(false);\n const isActionable = node.actionable !== false;\n const extraActions = additionalActions?.(node) ?? [];\n const primaryExtraActions = extraActions.filter((a) => a.placement === \"primary\");\n const overflowExtraActions = extraActions.filter((a) => a.placement !== \"primary\");\n const isPlayable = node.itemCount > 0;\n const isThisPlaying = playingNodeId === node.id;\n const playLabel = isThisPlaying && playState === \"playing\" ? \"Pause\" : isThisPlaying && playState === \"loading\" ? \"Loading...\" : \"Play\";\n const playIcon =\n isThisPlaying && playState === \"playing\" ? (\n <FaPause className=\"gtv-icon\" size=\"0.6em\" />\n ) : isThisPlaying && playState === \"loading\" ? (\n <FaSpinner className=\"gtv-icon gtv-icon--spin\" size=\"0.6em\" />\n ) : (\n <FaPlay className=\"gtv-icon\" size=\"0.6em\" />\n );\n\n return (\n <div className={[\"gtv-toolbar\", className].filter(Boolean).join(\" \")}>\n <button\n type=\"button\"\n className={`gtv-toolbar-btn${!isPlayable ? \" gtv-toolbar-btn--disabled\" : \"\"}`}\n title={playLabel}\n aria-label={playLabel}\n disabled={!isPlayable}\n onClick={() => onPlayPause?.(node.id)}\n >\n {playIcon}\n </button>\n\n {isActionable && (\n <>\n <button\n type=\"button\"\n className=\"gtv-toolbar-btn\"\n title=\"Add sub-genre\"\n aria-label=\"Add sub-genre\"\n onClick={() => onAddChild?.(node.id)}\n >\n <FaPlus className=\"gtv-icon\" size=\"0.6em\" />\n </button>\n {primaryExtraActions.map((action) => {\n const enabled = action.enabled ? action.enabled(node) : true;\n const label = action.label(node);\n return (\n <button\n key={action.key}\n type=\"button\"\n className={`gtv-toolbar-btn${!enabled ? \" gtv-toolbar-btn--disabled\" : \"\"}`}\n title={label}\n aria-label={label}\n disabled={!enabled}\n onClick={(event) => action.onClick(event, node)}\n >\n {action.icon(node)}\n </button>\n );\n })}\n <div className=\"gtv-toolbar-overflow\">\n <button\n type=\"button\"\n className=\"gtv-toolbar-btn\"\n title=\"More actions\"\n aria-label=\"More actions\"\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n onClick={() => setMenuOpen((open) => !open)}\n >\n <MdMoreVert size=\"0.6em\" />\n </button>\n {menuOpen && (\n <div className=\"gtv-menu-card gtv-toolbar-overflow-menu\">\n <button\n type=\"button\"\n className=\"gtv-menu-row\"\n onClick={() => {\n setMenuOpen(false);\n onRenameRequest?.(node);\n }}\n >\n <span className=\"gtv-menu-row-icon\">\n <MdModeEdit className=\"gtv-icon\" size={13} />\n </span>\n <span className=\"gtv-menu-row-label\">Rename</span>\n </button>\n <button\n type=\"button\"\n className=\"gtv-menu-row\"\n onClick={() => {\n setMenuOpen(false);\n onReparentRequest?.(node);\n }}\n >\n <span className=\"gtv-menu-row-icon\">\n <PiGraphFill className=\"gtv-icon\" size={13} />\n </span>\n <span className=\"gtv-menu-row-label\">Change parent</span>\n </button>\n {overflowExtraActions.map((action) => {\n const enabled = action.enabled ? action.enabled(node) : true;\n return (\n <button\n key={action.key}\n type=\"button\"\n className={`gtv-menu-row${action.danger ? \" gtv-menu-row--danger\" : \"\"}${!enabled ? \" gtv-menu-row--disabled\" : \"\"}`}\n disabled={!enabled}\n onClick={(event) => {\n setMenuOpen(false);\n action.onClick(event, node);\n }}\n >\n <span className=\"gtv-menu-row-icon\">{action.icon(node)}</span>\n <span className=\"gtv-menu-row-label\">{action.label(node)}</span>\n </button>\n );\n })}\n <div className=\"gtv-menu-divider\" />\n <button\n type=\"button\"\n className=\"gtv-menu-row gtv-menu-row--danger\"\n onClick={() => {\n setMenuOpen(false);\n onDeleteRequest?.(node);\n }}\n >\n <span className=\"gtv-menu-row-icon\">\n <FaTrashAlt className=\"gtv-icon\" size={13} />\n </span>\n <span className=\"gtv-menu-row-label\">Delete</span>\n </button>\n </div>\n )}\n </div>\n </>\n )}\n </div>\n );\n}\n","import { useEffect, useRef } from \"react\";\nimport { MdClose } from \"react-icons/md\";\n\nimport { isEscapeOwnedElsewhere } from \"./InfoPanel\";\nimport { InfoPanelExpandButton } from \"./InfoPanelExpandButton\";\nimport { GenreTreeOverlayPanel } from \"./types\";\n\n/** Consumer-supplied panel reusing the info panel's classes, so it covers exactly the same area —\n * see GenreTreeProps.overlayPanel. */\nexport function OverlayPanel({\n title,\n content,\n onClose,\n expanded: controlledExpanded,\n onExpandedChange,\n extraContent,\n collapsed = false,\n hideExpandButton = false,\n hideTitle = false,\n hideClose = false,\n}: GenreTreeOverlayPanel) {\n const panelRef = useRef<HTMLDivElement>(null);\n const expandable = !!onExpandedChange;\n const expanded = expandable ? !!controlledExpanded : true;\n\n // `inert` is a boolean attribute in React 19 but an unknown string one in 18 (the peer range covers both).\n useEffect(() => {\n panelRef.current?.toggleAttribute(\"inert\", collapsed);\n }, [collapsed]);\n\n useEffect(() => {\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\" || isEscapeOwnedElsewhere(event)) return;\n if (expanded && onExpandedChange) onExpandedChange(false);\n else onClose();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [expanded, onExpandedChange, onClose]);\n\n const className = [\n \"gtv-info-panel\",\n expandable ? \"gtv-info-panel--left\" : \"gtv-info-panel--right gtv-info-panel--overlay\",\n expanded && \"gtv-info-panel--expanded\",\n expanded && !extraContent && \"gtv-info-panel--expanded-solo\",\n collapsed && \"gtv-info-panel--collapsed\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <div className={className} ref={panelRef}>\n <div className=\"gtv-info-panel-header\">\n <span className={`gtv-info-panel-title${hideTitle ? \" gtv-info-panel-title--hidden\" : \"\"}`}>{title}</span>\n {onExpandedChange && !hideExpandButton && (\n <InfoPanelExpandButton expanded={expanded} onExpandedChange={onExpandedChange} />\n )}\n {!hideClose && (\n <button type=\"button\" className=\"gtv-info-panel-close\" onClick={onClose} aria-label=\"Close\">\n <MdClose className=\"gtv-icon\" size={16} />\n </button>\n )}\n </div>\n <div className=\"gtv-info-panel-body\">\n <div className=\"gtv-info-panel-aside gtv-overlay-content\">{content}</div>\n {extraContent && <div className=\"gtv-info-panel-primary gtv-overlay-content\">{extraContent}</div>}\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as d3 from \"d3\";\n\nimport { GenreTree } from \"./GenreTree\";\nimport { calculateRootAnchorClearance } from \"./NodeHelper\";\nimport { computeAncestorChain, findRootId, groupNodesByRoot } from \"./root-grouping\";\nimport {\n buildWheelSectorGradient,\n calculateWheelRadius,\n computeRotationForSelection,\n getChipAngle,\n getWheelDividerAngle,\n} from \"./wheel-geometry\";\nimport { FloatingControls } from \"./FloatingControls\";\nimport { usePanZoom } from \"./use-pan-zoom\";\nimport { queryTreeContentElements } from \"./zoom-pan\";\nimport { useNodeInfoPanel } from \"./use-node-info-panel\";\nimport { resolveInfoPanelObscuredArea } from \"./info-panel-geometry\";\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\nimport { GenreTreeNode, GenreTreeProps, TreeOrientation } from \"./types\";\nimport {\n ACCENT_TEXT_COLOR,\n calculateNodeDimensions,\n calculateNodeFontSize,\n getGenreTreeColor,\n getItemCountRange,\n hexToRgba,\n INFO_PANEL_WIDTH,\n MAX_NODE_HEIGHT,\n MAX_NODE_WIDTH,\n PER_TREE_ACCENT_DOT,\n ROOT_SECTOR_FILL_OPACITY,\n WHEEL_RADIUS,\n WHEEL_ROTATION_EASING,\n WHEEL_ROTATION_TRANSITION_MS,\n ZOOM_FOCUS_SCALE,\n} from \"./constants\";\n\nexport interface WheelCoreProps\n extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the selected root changes — on mount with the default selection, and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. Stays upright and\n * fixed regardless of the wheel's rotation. */\n centerLabel?: string;\n /** Which edge of the container the wheel hugs. \"bottom\" (GenreTreeWheel) lands the selected\n * chip top-center and grows the subtree upward; \"left\" (GenreTreeWheelRight) lands it\n * right-center and grows the subtree rightward. */\n direction: \"bottom\" | \"left\";\n /** When false, clicking a chip still selects its root and swaps in its subtree, but the wheel\n * itself stays at its current rotation instead of spinning the selected chip to the anchor.\n * Defaults to true. */\n allowWheelRotation?: boolean;\n}\n\n/**\n * Shared implementation behind both `GenreTreeWheel` (`direction=\"bottom\"`) and\n * `GenreTreeWheelRight` (`direction=\"left\"`): all root genres distributed evenly around a wheel\n * hugging one edge of its container. Clicking a chip rotates the wheel so that root lands at the\n * anchor for that edge, and swaps in its subtree — rendered as a single non-interactive\n * `<GenreTree>` growing away from that anchor. Only the selected root's subtree is ever mounted.\n */\nexport function WheelCore({\n nodes,\n className,\n onRootSelect,\n centerLabel,\n direction,\n allowWheelRotation = true,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n showToolbar = true,\n renderInfoPanelBody,\n onSelectedNodeChange,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n wheelZoom,\n defaultViewport,\n onViewportChange,\n controlsOrientation = \"vertical\",\n floatingControlsLeading,\n busy,\n selectedNodeId,\n}: WheelCoreProps) {\n const treeOrientation: TreeOrientation =\n direction === \"left\" ? \"horizontal-anchored\" : \"vertical\";\n // CSS `rotate()` + `translateY(-radius)` convention: local angle 0°=top, 90°=right, 180°=bottom,\n // 270°=left — measured against the disc's own unrotated box, since its CSS `top`/`left`/`right`\n // position (not its `rotate()` transform, which only spins rendered content around the box's\n // center) is what's pinned to the container's anchor point. The bottom-hugging wheel's box has\n // its top-center pinned there (local angle 0). The left-hugging wheel's box is pinned by its\n // *right* edge (`right: 0` in the `--left` CSS below), i.e. its right-center point — local\n // angle 90 — so the selection has to land at 90, not at the \"left\" angle (270) the disc bulges\n // toward.\n const landingAngle = direction === \"left\" ? 90 : 0;\n const chipHalfExtentVar =\n direction === \"left\"\n ? \"--gtv-wheel-chip-half-width\"\n : \"--gtv-wheel-chip-half-height\";\n\n const groups = useMemo(() => groupNodesByRoot(nodes), [nodes]);\n const rootColorOf = (nodeId: string) => {\n const rootId = findRootId(nodeId, nodes) ?? nodeId;\n return getGenreTreeColor(nodes.find((n) => n.id === rootId)?.wikidataId);\n };\n\n // The largest a chip can ever render is MAX_NODE_WIDTH (the root with the highest itemCount in\n // range always sits at the top of the scale — see calculateNodeDimensions), so that's the\n // width every neighboring pair of chips needs clearance for, regardless of which root actually\n // ends up at that size. WHEEL_RADIUS is a floor: the wheel never shrinks below its default, only\n // grows to fit more/larger chips.\n const wheelRadius = useMemo(\n () => calculateWheelRadius(groups.length, MAX_NODE_WIDTH, WHEEL_RADIUS),\n [groups.length],\n );\n const wheelViewportHeight = wheelRadius * 2 + MAX_NODE_HEIGHT / 2;\n\n const rootItemCountById = useMemo(\n () => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),\n [groups],\n );\n const [selectedRootId, setSelectedRootId] = useState<string | null>(\n groups[0]?.root.id ?? null,\n );\n // The default selection (chip index 0) must start already rotated to the anchor, not just at\n // 0deg — 0 only happens to be correct for direction=\"bottom\", where landingAngle (0) coincides\n // with chip 0's own raw angle (getChipAngle(0, n) is always 0). For direction=\"left\"\n // (landingAngle 270) those differ, so the initial rotation has to be computed the same way a\n // click's rotation is.\n const [rotationDeg, setRotationDeg] = useState(() =>\n groups.length > 0\n ? computeRotationForSelection(\n 0,\n getChipAngle(0, groups.length),\n landingAngle,\n )\n : 0,\n );\n const viewportRef = useRef<HTMLDivElement>(null);\n // One shared pan/zoom transform, applied to the stage below that anchors both the tree and the\n // wheel to the same point — so panning/zooming moves them together with no JS sync required.\n const stageRef = useRef<HTMLDivElement>(null);\n const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });\n const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });\n // Fit-to-frame targets: the circle (not .gtv-wheel, which rotates and would inflate its own\n // axis-aligned bounding box) and the tree anchor (only mounted once a root is selected).\n const wheelCircleRef = useRef<HTMLDivElement>(null);\n const treeAnchorRef = useRef<HTMLDivElement>(null);\n\n // Falls back to the first root without writing state back when the explicitly selected root\n // disappears from `nodes` — avoids a setState-in-effect cascading render for derived state.\n const effectiveRootId = groups.some(\n (group) => group.root.id === selectedRootId,\n )\n ? selectedRootId\n : (groups[0]?.root.id ?? null);\n\n // Read via a ref rather than depending on `onRootSelect` directly — consumers commonly pass an\n // inline callback, which would otherwise re-fire this effect (and any state it sets) every render.\n const onRootSelectRef = useRef(onRootSelect);\n useEffect(() => {\n onRootSelectRef.current = onRootSelect;\n });\n\n useEffect(() => {\n if (effectiveRootId) onRootSelectRef.current?.(effectiveRootId);\n }, [effectiveRootId]);\n\n // Starts the view fit to the wheel + selected subtree instead of at scale 1 / pan (0, 0) —\n // guarded so it only fires once the tree anchor has actually rendered content, and never again\n // afterward so it doesn't fight the user's own pan/zoom on later selections.\n const hasInitialFitRef = useRef(false);\n useEffect(() => {\n if (hasInitialFitRef.current) return;\n const elements = [\n wheelCircleRef.current,\n ...queryTreeContentElements(treeAnchorRef.current),\n ];\n if (!elements.some(Boolean)) return;\n hasInitialFitRef.current = true;\n panZoom.fitToFrame(elements, { silent: true });\n });\n\n const selectedGroup =\n groups.find((group) => group.root.id === effectiveRootId) ?? null;\n\n // Root chip size is proportional to itemCount relative to the other roots on the wheel, not\n // an absolute scale — the root with the fewest items always renders at MIN size and the one\n // with the most always at MAX, regardless of the actual counts involved.\n const rootItemCountRange = useMemo(\n () =>\n getItemCountRange(\n groups.map((group) => ({\n itemCount: rootItemCountById.get(group.root.id)!,\n })),\n ),\n [groups, rootItemCountById],\n );\n\n // The selected chip is centered on its wheel anchor point (translate(-50%, -50%)) so it stays\n // centered on the circle like every other chip, but the tree's root lands with its near edge\n // exactly at that same anchor point (see .gtv-wheel-tree-anchor). Left alone, that mismatch makes\n // the chip overlap the tree's near edge — offsetting the anchor by the chip's own half-extent\n // along the growth axis clears it, so the root->depth1 gap reads the same as any other\n // consecutive-depth gap. See calculateRootAnchorClearance's doc comment (and\n // GenreTreeWheelRadialBase for the equivalent radial-wheel computation) for why that offset needs\n // both the hidden root's own local half-extent and the visible chip's cross-root display\n // half-extent, not just one or the other.\n const rootChipHalfExtent = selectedGroup\n ? calculateRootAnchorClearance(\n d3,\n selectedGroup.nodes,\n rootItemCountById.get(selectedGroup.root.id)!,\n rootItemCountRange,\n direction === \"left\" ? \"WIDTH\" : \"HEIGHT\",\n )\n : 0;\n\n // Colors every root's chip index (i.e. getChipAngle(i, n)'s ordering) matches groups' own order,\n // so each color stop lines up with the sector buildWheelSectorGradient carves out for it.\n const sectorFillGradient = useMemo(\n () =>\n buildWheelSectorGradient(\n groups.map((group) =>\n hexToRgba(getGenreTreeColor(group.root.wikidataId), ROOT_SECTOR_FILL_OPACITY),\n ),\n ),\n [groups],\n );\n\n const handleChipClick = (rootId: string, angle: number) => {\n setSelectedRootId(rootId);\n if (allowWheelRotation) {\n setRotationDeg((current) =>\n computeRotationForSelection(current, angle, landingAngle),\n );\n }\n };\n\n // An externally-controlled selection (e.g. from a search result) may belong to a root other than\n // the one currently mounted — swap it in the same way a chip click would before trying to locate\n // the node's DOM element below. Adjusted during render rather than in an effect so the swap is\n // part of the same commit (see React's \"adjusting state when a prop changes\"), and only on an\n // actual change of the prop so a later chip click isn't undone.\n const [appliedSelectedNodeId, setAppliedSelectedNodeId] =\n useState(selectedNodeId);\n if (selectedNodeId !== appliedSelectedNodeId) {\n setAppliedSelectedNodeId(selectedNodeId);\n const owningGroupIndex = selectedNodeId\n ? groups.findIndex((group) =>\n group.nodes.some((node) => node.id === selectedNodeId),\n )\n : -1;\n if (\n owningGroupIndex !== -1 &&\n groups[owningGroupIndex].root.id !== effectiveRootId\n ) {\n handleChipClick(\n groups[owningGroupIndex].root.id,\n getChipAngle(owningGroupIndex, groups.length),\n );\n }\n }\n\n // Mirrors the onNodeClick/onSelectNode handling below: once the owning root's subtree is\n // mounted, center the viewport on the externally-selected node and open its info panel exactly\n // as a direct click would.\n useEffect(() => {\n if (!selectedNodeId || panel?.node.id === selectedNodeId) return;\n const targetNode = nodes.find((node) => node.id === selectedNodeId);\n const element = treeAnchorRef.current?.querySelector(\n `#group-${CSS.escape(selectedNodeId)}`,\n );\n if (!targetNode || !element) return;\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(targetNode, element, viewportRef.current, { notify: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps -- panZoom re-created on pan/zoom; guarded by the panel?.node.id check above\n }, [selectedNodeId, effectiveRootId, nodes, panel, showNodeInfo]);\n\n return (\n <div\n ref={viewportRef}\n aria-busy={busy}\n className={[\n \"gtv-wheel-container\",\n direction === \"left\" && \"gtv-wheel-container--left\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n \"--gtv-wheel-radius\": `${wheelRadius}px`,\n \"--gtv-wheel-viewport-height\": `${wheelViewportHeight}px`,\n \"--gtv-wheel-rotation-transition-ms\": `${WHEEL_ROTATION_TRANSITION_MS}ms`,\n \"--gtv-wheel-rotation-easing\": WHEEL_ROTATION_EASING,\n touchAction: panZoom.touchAction,\n } as React.CSSProperties\n }\n onPointerDown={panZoom.handlePointerDown}\n >\n {/* height: 100% (not just width) so this is the containing block .gtv-wheel-stage's own\n `bottom: var(--gtv-wheel-viewport-height)` measures against — otherwise, with only an\n absolutely-positioned child, this wrapper's height collapses to 0 and the stage's anchor\n point would land at the container's top instead of its bottom. The transform is purely\n visual and doesn't affect this box-model sizing. */}\n <div\n ref={stageRef}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n transform: panZoom.transform,\n transformOrigin: \"0 0\",\n }}\n >\n <div\n className=\"gtv-wheel-stage\"\n style={\n {\n [chipHalfExtentVar]: `${rootChipHalfExtent}px`,\n } as React.CSSProperties\n }\n >\n {selectedGroup && (\n <div className=\"gtv-wheel-tree-anchor\" ref={treeAnchorRef}>\n <GenreTree\n key={selectedGroup.root.id}\n nodes={selectedGroup.nodes}\n itemCountLabel={itemCountLabel}\n orientation={treeOrientation}\n hideRoot\n interactive={false}\n rootColor={getGenreTreeColor(selectedGroup.root.wikidataId)}\n playingNodeId={playingNodeId}\n playState={playState}\n reparentingNodeId={reparentingNodeId}\n onPlayPause={onPlayPause}\n onAddChild={onAddChild}\n onRenameRequest={onRenameRequest}\n onDeleteRequest={onDeleteRequest}\n onReparentRequest={onReparentRequest}\n onReparent={onReparent}\n onNodeClick={(data, event) => {\n const clickedElement = event.currentTarget as Element | null;\n panZoom.centerOnElement(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n }}\n additionalActions={additionalActions}\n showToolbar={showToolbar}\n selectedNodeId={panel?.node.id ?? null}\n />\n </div>\n )}\n\n <div className=\"gtv-wheel-circle\" ref={wheelCircleRef} />\n\n {centerLabel && (\n <div className=\"gtv-wheel-center-label\">{centerLabel}</div>\n )}\n\n <div\n className=\"gtv-wheel\"\n style={\n {\n \"--gtv-wheel-rotation\": `${rotationDeg}deg`,\n } as React.CSSProperties\n }\n >\n {sectorFillGradient && (\n <div\n className=\"gtv-wheel-sector-fill\"\n style={\n {\n \"--gtv-sector-fill-gradient\": sectorFillGradient,\n } as React.CSSProperties\n }\n />\n )}\n\n {groups.length > 1 &&\n groups.map((group, index) => (\n <div\n key={`divider-${group.root.id}`}\n className=\"gtv-wheel-divider\"\n style={\n {\n \"--gtv-divider-angle\": `${getWheelDividerAngle(index, groups.length)}deg`,\n } as React.CSSProperties\n }\n />\n ))}\n\n {groups.map((group, index) => {\n const angle = getChipAngle(index, groups.length);\n const selected = group.root.id === effectiveRootId;\n const chipColor = getGenreTreeColor(group.root.wikidataId);\n const rootItemCount = rootItemCountById.get(group.root.id)!;\n const dimensions = calculateNodeDimensions(\n rootItemCount,\n rootItemCountRange,\n );\n const fontSize = calculateNodeFontSize(\n rootItemCount,\n rootItemCountRange,\n );\n return (\n <div\n key={group.root.id}\n className=\"gtv-wheel-slot\"\n style={\n { \"--gtv-chip-angle\": `${angle}deg` } as React.CSSProperties\n }\n >\n <div className=\"gtv-wheel-chip-anchor\">\n <button\n type=\"button\"\n id={`group-${group.root.id}`}\n className={[\n \"gtv-wheel-chip\",\n selected && \"gtv-wheel-chip--selected\",\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n width: dimensions.WIDTH,\n // Height comes from --gtv-wheel-chip-base-height (see .gtv-wheel-chip in\n // styles.css), not an inline `height`/`minHeight` — an inline `height` always\n // wins the cascade over the CSS class's `height: calc(...)` hover override\n // (inline style beats any stylesheet rule), silently pinning the box and\n // breaking the hover growth again, same failure mode as before.\n \"--gtv-wheel-chip-base-height\": `${dimensions.HEIGHT}px`,\n \"--gtv-chip-color\": chipColor,\n } as React.CSSProperties\n }\n onClick={(event) => {\n panZoom.centerOnElement(\n event.currentTarget,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(event.currentTarget, viewportRef.current, INFO_PANEL_WIDTH),\n );\n handleChipClick(group.root.id, angle);\n if (!reparentingNodeId) {\n showNodeInfo(\n group.root,\n event.currentTarget,\n viewportRef.current,\n );\n onNodeClick?.(group.root, event.nativeEvent);\n }\n }}\n >\n {PER_TREE_ACCENT_DOT && (\n <span className=\"gtv-wheel-chip-dot\" />\n )}\n <span\n className=\"gtv-node-label gtv-node-label--root\"\n style={{ fontSize }}\n >\n {group.root.name}\n </span>\n </button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {panel && (\n <InfoPanel\n node={panel.node}\n fill={rootColorOf(panel.node.id)}\n textColor={ACCENT_TEXT_COLOR}\n parentNode={(() => {\n const parent = nodes.find((n) => n.id === panel.node.parentId);\n return parent\n ? {\n node: parent,\n fill: rootColorOf(parent.id),\n textColor: ACCENT_TEXT_COLOR,\n }\n : null;\n })()}\n childNodes={nodes\n .filter((n) => n.parentId === panel.node.id)\n .map((n) => {\n const rootColor = rootColorOf(n.id);\n // This wheel's inner subtree always renders via <GenreTree hideRoot ... />, so every\n // visible node — including every child listed here — gets a solid rootColor fill.\n return { node: n, fill: rootColor, textColor: ACCENT_TEXT_COLOR };\n })}\n ancestorNodes={computeAncestorChain(nodes, panel.node.id).map(\n (n): InfoPanelChild => ({\n node: n,\n fill: rootColorOf(n.id),\n textColor: ACCENT_TEXT_COLOR,\n }),\n )}\n side={panel.side}\n onClose={closeNodeInfo}\n onSelectNode={(id) => {\n const targetNode = nodes.find((n) => n.id === id)!;\n const element = viewportRef.current!.querySelector(\n `#group-${CSS.escape(id)}`,\n );\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(\n targetNode,\n element ?? viewportRef.current,\n viewportRef.current,\n );\n }}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n renderInfoPanelBody={renderInfoPanelBody}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n }\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n\n <FloatingControls\n panZoom={panZoom}\n orientation={controlsOrientation}\n leading={floatingControlsLeading}\n busy={busy}\n onFit={() => panZoom.fitToFrame([\n wheelCircleRef.current,\n ...queryTreeContentElements(treeAnchorRef.current),\n ])}\n />\n </div>\n );\n}\n","import { GenreTreeNode } from \"./types\";\n\nexport interface GenreTreeRootGroup {\n root: GenreTreeNode;\n nodes: GenreTreeNode[];\n}\n\n/**\n * Walks `nodeId`'s `parentId` chain up through `nodes` to find its top-level ancestor (a node\n * with `parentId === null`). Returns `null` if the chain terminates on a `parentId` absent from\n * `nodes` (dangling reference) or `nodeId` itself isn't in `nodes`.\n */\nexport function findRootId(nodeId: string, nodes: GenreTreeNode[]): string | null {\n const nodeById = new Map(nodes.map((node) => [node.id, node]));\n\n const rootIdOf = (id: string): string | null => {\n const node = nodeById.get(id);\n return !node ? null : node.parentId === null ? node.id : rootIdOf(node.parentId);\n };\n\n return rootIdOf(nodeId);\n}\n\n/**\n * Groups a flat node list by top-level ancestor (a node with `parentId === null`), walking each\n * node's `parentId` chain up to find which root it belongs to. A node whose chain terminates on a\n * `parentId` absent from `nodes` (dangling reference) belongs to no group.\n */\nexport function groupNodesByRoot(nodes: GenreTreeNode[]): GenreTreeRootGroup[] {\n const nodeById = new Map(nodes.map((node) => [node.id, node]));\n const rootIdCache = new Map<string, string | null>();\n\n const rootIdOf = (nodeId: string): string | null => {\n if (rootIdCache.has(nodeId)) return rootIdCache.get(nodeId)!;\n const node = nodeById.get(nodeId);\n const rootId = !node ? null : node.parentId === null ? node.id : rootIdOf(node.parentId);\n rootIdCache.set(nodeId, rootId);\n return rootId;\n };\n\n const nodesByRootId = new Map<string, GenreTreeNode[]>();\n for (const node of nodes) {\n const rootId = rootIdOf(node.id);\n if (rootId === null) continue;\n const group = nodesByRootId.get(rootId);\n if (group) group.push(node);\n else nodesByRootId.set(rootId, [node]);\n }\n\n return nodes\n .filter((node) => node.parentId === null)\n .map((root) => ({ root, nodes: nodesByRootId.get(root.id)! }));\n}\n\n/**\n * Walks upward from `parentId` through `nodes`, collecting every ancestor above it (root-first) —\n * excludes `parentId`'s own node, since callers already show the immediate parent separately.\n * Stops if the chain terminates on a `parentId` absent from `nodes` (dangling reference).\n */\nexport function computeAncestorChain(nodes: GenreTreeNode[], parentId: string | null): GenreTreeNode[] {\n const ancestors: GenreTreeNode[] = [];\n let current = nodes.find((n) => n.id === parentId);\n while (current && current.parentId !== null) {\n const parent = nodes.find((n) => n.id === current!.parentId);\n if (!parent) break;\n ancestors.unshift(parent);\n current = parent;\n }\n return ancestors;\n}\n","/** The static angle (CSS `rotate()` convention: 0° = top, clockwise) of the `index`-th of\n * `totalCount` chips, evenly spaced around the full circle before any wheel rotation is\n * applied. Every chip stays reachable regardless of spacing — the whole circle is always\n * on screen and any chip can be brought to the top by rotating the wheel. */\nexport function getChipAngle(index: number, totalCount: number): number {\n return index * (360 / totalCount);\n}\n\n/** Angle (CSS `rotate()` convention) of the boundary between the `index`-th chip and its next\n * neighbor clockwise, for `totalCount` evenly-spaced chips — the midpoint of their spacing, used\n * to draw a radial divider separating the two. */\nexport function getWheelDividerAngle(index: number, totalCount: number): number {\n return getChipAngle(index, totalCount) + 180 / totalCount;\n}\n\n/** CSS `conic-gradient(...)` value tinting each of `totalCount` evenly-spaced chips' angular\n * span with its own color (see ROOT_SECTOR_FILL_OPACITY / hexToRgba in constants.ts for producing\n * a translucent entry) — `colors[i]` must correspond to the chip at `getChipAngle(i, totalCount)`.\n * Hard stops (not a smooth blend) at each getWheelDividerAngle boundary, so a sector's fill lines\n * up exactly with its bounding divider lines. A single static gradient works here (unlike the\n * radial wheel variants) because this wheel's chips are evenly spaced and the whole `.gtv-wheel`\n * box rotates as one unit — nesting this inside that box lets it rotate for free instead of\n * needing its own per-sector rotation. Empty string (no fill) below 2 colors, matching the \"no\n * dividers for 1 root\" rule. */\nexport function buildWheelSectorGradient(colors: string[]): string {\n const n = colors.length;\n if (n < 2) return \"\";\n const span = 360 / n;\n // Starts the gradient at chip 0's own leading boundary so the first hard stop lands exactly on\n // a divider instead of splitting chip 0's own sector across the gradient's 0deg seam.\n const startAngle = getWheelDividerAngle(n - 1, n);\n const stops = colors.map((color, i) => `${color} ${i * span}deg ${(i + 1) * span}deg`).join(\", \");\n return `conic-gradient(from ${startAngle}deg, ${stops})`;\n}\n\n/** Minimum wheel radius so adjacent chips — each up to `maxChipWidth` wide in the worst case —\n * don't overlap. The chord between two adjacent chip anchor points on the circle\n * (2 * radius * sin(pi / rootCount)) must be at least `maxChipWidth` plus a small gap; solving\n * for radius gives the minimum that keeps every pair of neighbors clear. Falls back to\n * `baseRadius` when that's already big enough, or when there's only one chip and so no neighbor\n * to overlap. */\nexport function calculateWheelRadius(rootCount: number, maxChipWidth: number, baseRadius: number): number {\n if (rootCount <= 1) return baseRadius;\n const CHIP_GAP = 16;\n const requiredRadius = (maxChipWidth + CHIP_GAP) / (2 * Math.sin(Math.PI / rootCount));\n return Math.max(baseRadius, requiredRadius);\n}\n\nfunction mod360(value: number): number {\n return ((value % 360) + 360) % 360;\n}\n\n/**\n * Computes the wheel rotation (degrees) that brings the chip at `targetAngle` to `landingAngle`\n * (default 0, the top — used by the bottom-hugging wheel; 270, the left, lands a chip at the\n * right — used by the left-hugging wheel), taking the shortest path from `currentRotationDeg`.\n * The wheel's own rotation composes with each chip's static angle, so bringing `targetAngle` to\n * `landingAngle` requires a rotation congruent to `landingAngle - targetAngle` (mod 360).\n *\n * The result is never renormalized into [0, 360) — it keeps accumulating so a CSS\n * `transition: transform` never has to jump across a 359°→0° wraparound.\n */\nexport function computeRotationForSelection(\n currentRotationDeg: number,\n targetAngle: number,\n landingAngle = 0,\n): number {\n const targetMod = mod360(landingAngle - targetAngle);\n const delta = targetMod - mod360(currentRotationDeg);\n const shortestDelta = delta - 360 * Math.round(delta / 360);\n return currentRotationDeg + shortestDelta;\n}\n","\"use client\";\n\nimport { WheelCore } from \"./GenreTreeWheelBase\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\n\nexport interface GenreTreeWheelProps extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the selected root changes — on mount with the default selection, and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. Stays upright and\n * fixed regardless of the wheel's rotation. */\n centerLabel?: string;\n /** When false, clicking a chip still selects its root and swaps in its subtree, but the wheel\n * itself stays at its current rotation instead of spinning the selected chip to the anchor.\n * Defaults to true. */\n allowWheelRotation?: boolean;\n}\n\n/**\n * All root genres distributed evenly around a wheel hugging the bottom of its container.\n * Clicking a chip rotates the wheel so that root lands at the top-center, and swaps in its\n * subtree — rendered as a single vertical `<GenreTree>` growing upward from that anchor.\n * Only the selected root's subtree is ever mounted.\n */\nexport function GenreTreeWheel(props: GenreTreeWheelProps) {\n return <WheelCore {...props} direction=\"bottom\" />;\n}\n","\"use client\";\n\nimport { WheelCore } from \"./GenreTreeWheelBase\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\n\nexport interface GenreTreeWheelRightProps extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the selected root changes — on mount with the default selection, and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. Stays upright and\n * fixed regardless of the wheel's rotation. */\n centerLabel?: string;\n /** When false, clicking a chip still selects its root and swaps in its subtree, but the wheel\n * itself stays at its current rotation instead of spinning the selected chip to the anchor.\n * Defaults to true. */\n allowWheelRotation?: boolean;\n}\n\n/**\n * All root genres distributed evenly around a wheel hugging the left edge of its container.\n * Clicking a chip rotates the wheel so that root lands at the right-center, and swaps in its\n * subtree — rendered as a single `<GenreTree>` growing rightward from that anchor.\n * Only the selected root's subtree is ever mounted.\n */\nexport function GenreTreeWheelRight(props: GenreTreeWheelRightProps) {\n return <WheelCore {...props} direction=\"left\" />;\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as d3 from \"d3\";\n\nimport { GenreTreeRootGroup, groupNodesByRoot } from \"./root-grouping\";\nimport { useTreeIndex } from \"./use-tree-index\";\nimport {\n buildCoreHierarchy,\n calculateCoreSubtreeRadialExtent,\n computeCoreRadialLayout,\n} from \"./core-radial-layout\";\nimport {\n getRadialPointOnCircle,\n POP_WEDGE_SPAN_DEGREES,\n renderPopSubtree,\n} from \"./pop-core-radial-layout\";\nimport { splitRootGroupBySide } from \"./pop-core-split\";\nimport {\n buildSectorClipPathPolygon,\n calculateWheelRadiusForAngles,\n computeRadialLayout,\n computeSectorWidths,\n RadialSlot,\n subdivideWedge,\n} from \"./radial-wheel-geometry\";\nimport { FloatingControls } from \"./FloatingControls\";\nimport { usePanZoom } from \"./use-pan-zoom\";\nimport { useNodeInfoPanel } from \"./use-node-info-panel\";\nimport { resolveInfoPanelObscuredArea } from \"./info-panel-geometry\";\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\nimport {\n ACCENT_TEXT_COLOR,\n calculateNodeDimensions,\n calculateNodeFontSize,\n getGenreTreeColor,\n getItemCountRange,\n hexToRgba,\n INFO_PANEL_WIDTH,\n MAX_NODE_WIDTH,\n PER_TREE_ACCENT_DOT,\n POP_TREE_DEPTH_RADIAL_SPACING,\n ROOT_SECTOR_FILL_OPACITY,\n WHEEL_RADIUS,\n WHEEL_ROTATION_EASING,\n WHEEL_ROTATION_TRANSITION_MS,\n ZOOM_FOCUS_SCALE,\n} from \"./constants\";\n\nexport interface WheelRadialCoreProps\n extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the top (just-clicked) root changes — on mount with the default selection,\n * and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. */\n centerLabel?: string;\n}\n\n// The wheel always lands the just-clicked root on the right (matches GenreTreeWheelRight's own\n// landingAngle=90 convention) — see computeRadialLayout's doc comment for why.\nconst LANDING_ANGLE = 90;\n\n// Unwraps computeRadialLayout's [0, 360)-wrapped angles into each root's own continuous\n// (possibly >360 or negative) angle, nearest to its `previous` value — see the doc comment where\n// this is used in WheelRadialCore for why plain wrapped angles cause a wrong-direction transition.\nfunction computeContinuousAngles(\n groups: GenreTreeRootGroup[],\n layout: RadialSlot[],\n previous: Map<string, number>,\n): Map<string, number> {\n const angles = new Map<string, number>();\n groups.forEach((group, index) => {\n const rawAngle = layout[index]?.angle ?? 0;\n const previousAngle = previous.get(group.root.id);\n const laps =\n previousAngle === undefined\n ? 0\n : Math.round((previousAngle - rawAngle) / 360);\n angles.set(group.root.id, rawAngle + 360 * laps);\n });\n return angles;\n}\n\n/**\n * All root genres distributed around a full circle centered in its container, each one's angular\n * width proportional to its own subtree's node count (see `computeRadialLayout`), every one of\n * them fully developed (full subtree mounted) at once. Clicking any chip re-lays-out the whole\n * ring so that root lands on the right and recalculates every other root's angle from scratch.\n * Unlike `WheelCore`, there's no single wheel-wide rotation to animate — every root's angle is\n * recomputed fresh from `(rootWeights, topIndex, LANDING_ANGLE)` each render, then unwrapped\n * per chip (see `continuousAngleByRootId`) so each chip's own CSS transition always takes its own\n * shortest path instead of snapping through a 360deg/0deg wraparound.\n *\n * Each root's own subtree fans outward from the wheel's circle in polar coordinates (see\n * `computeCoreRadialLayout`), within an 80deg wedge centered on the root's angle — the same wedge\n * geometry and shared `<svg>` layer used by the pop/core wheel.\n */\nexport function WheelRadialCore({\n nodes,\n className,\n onRootSelect,\n centerLabel,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n showToolbar = true,\n renderInfoPanelBody,\n onSelectedNodeChange,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n wheelZoom,\n defaultViewport,\n onViewportChange,\n controlsOrientation = \"vertical\",\n floatingControlsLeading,\n busy,\n selectedNodeId,\n}: WheelRadialCoreProps) {\n const index = useTreeIndex(nodes);\n const groups = useMemo(() => groupNodesByRoot(nodes), [nodes]);\n const rootColorOf = (node: GenreTreeNode) =>\n getGenreTreeColor(index.byId.get(index.rootIdById.get(node.id) ?? node.id)?.wikidataId);\n\n const [topRootId, setTopRootId] = useState<string | null>(\n groups[0]?.root.id ?? null,\n );\n const viewportRef = useRef<HTMLDivElement>(null);\n const stageRef = useRef<HTMLDivElement>(null);\n const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });\n const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });\n // Read via a ref inside the D3 render effect below rather than depending on `panZoom` directly —\n // its identity changes every render, which would otherwise re-run (and re-mount the whole D3\n // tree) on every pan/zoom, exactly what that effect's own dependency list is designed to avoid.\n const centerOnElementRef = useRef(panZoom.centerOnElement);\n useEffect(() => {\n centerOnElementRef.current = panZoom.centerOnElement;\n });\n const showNodeInfoRef = useRef(showNodeInfo);\n useEffect(() => {\n showNodeInfoRef.current = showNodeInfo;\n });\n const wheelCircleRef = useRef<HTMLDivElement>(null);\n const coreSvgRef = useRef<SVGSVGElement>(null);\n\n // Falls back to the first root without writing state back when the explicitly selected root\n // disappears from `nodes` — avoids a setState-in-effect cascading render for derived state.\n const effectiveTopRootId = groups.some((group) => group.root.id === topRootId)\n ? topRootId\n : (groups[0]?.root.id ?? null);\n const topIndex = Math.max(\n groups.findIndex((group) => group.root.id === effectiveTopRootId),\n 0,\n );\n\n // Weighted by each root's own rendered core branch (splitRootGroupBySide), not group.nodes.length\n // — group.nodes also includes the root's pop branch (see coreHierarchyByRootId below), which this\n // component never renders, so weighting by it would inflate a root's slice past what it actually\n // draws whenever it has a large hidden pop branch.\n const rootWeights = useMemo(\n () => groups.map((group) => splitRootGroupBySide(group).coreNodes.length),\n [groups],\n );\n\n const layout = useMemo(\n () => computeRadialLayout(rootWeights, topIndex, LANDING_ANGLE),\n [rootWeights, topIndex],\n );\n\n // computeRadialLayout always returns angles wrapped to [0, 360) — re-rendering with a fresh\n // wrapped value on every selection change means a chip landing back near 0deg (top) has to\n // transition from e.g. 270deg down through 90/0, i.e. always counterclockwise, regardless of\n // which way the ring conceptually just turned. Track each chip's own last displayed (unwrapped)\n // angle and add/subtract full turns so its CSS transition always takes its own shortest path.\n // Recomputed by comparing against `layout` right in the render body (React's sanctioned\n // \"adjusting state during render\" pattern — see the React docs on storing info from previous\n // renders) rather than in an effect, so there's no extra committed frame with stale angles.\n const [angleMemo, setAngleMemo] = useState(() => ({\n layout,\n angles: computeContinuousAngles(groups, layout, new Map()),\n }));\n if (angleMemo.layout !== layout) {\n setAngleMemo({\n layout,\n angles: computeContinuousAngles(groups, layout, angleMemo.angles),\n });\n }\n const continuousAngleByRootId = angleMemo.angles;\n\n const rootItemCountById = useMemo(\n () => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),\n [groups],\n );\n\n const rootItemCountRange = useMemo(\n () =>\n getItemCountRange(\n groups.map((group) => ({\n itemCount: rootItemCountById.get(group.root.id)!,\n })),\n ),\n [groups, rootItemCountById],\n );\n\n // Node cards scale against the whole tree's item counts, not just their own subtree's — a\n // shallow root's few nodes can have a narrow itemCount spread that would otherwise get stretched\n // across the full size range and render wildly inconsistent card sizes next to deeper siblings.\n const wheelItemCountRange = useMemo(() => getItemCountRange(nodes), [nodes]);\n\n // Real angular sector each ring root owns, proportional to its own weight (rootWeights) out of\n // the total — same widths computeRadialLayout placed chips with, so a root's wedge below is\n // guaranteed to fit within its actual sector without spilling into a neighbor's, regardless of\n // how uneven neighboring roots' weights are (see computeSectorWidths's doc comment).\n const sectorSpanByRootId = useMemo(() => {\n const map = new Map<string, number>();\n if (groups.length <= 1) return map;\n const widths = computeSectorWidths(rootWeights);\n groups.forEach((group, index) => {\n map.set(group.root.id, widths[index]);\n });\n return map;\n }, [groups, rootWeights]);\n\n const wedgeSpanForRoot = useCallback(\n (rootId: string) =>\n Math.min(\n POP_WEDGE_SPAN_DEGREES,\n sectorSpanByRootId.get(rootId) ?? POP_WEDGE_SPAN_DEGREES,\n ),\n [sectorSpanByRootId],\n );\n\n // Each root's own single angle on the ring — a link from the root down to any of its branches\n // must originate from the root's own unique chip position, not a branch's sub-wedge angle.\n const rootAngleByRootId = useMemo(() => {\n const map = new Map<string, number>();\n groups.forEach((group, index) => {\n map.set(group.root.id, layout[index]?.angle ?? 0);\n });\n return map;\n }, [groups, layout]);\n\n // Only roots that actually have children get a hierarchy built — a childless root has nothing\n // to fan outward. A root's pop-side branch (splitRootGroupBySide) is excluded here the same way\n // GenreTreeWheelRadialPopCoreBase excludes it, since this component has no separate pop\n // rendering path and mixing both branches' direct children into buildCoreHierarchy would leave\n // more than one node with no in-set parent — d3.stratify() rejects that as \"multiple roots\". A\n // root's wedge (capped at POP_WEDGE_SPAN_DEGREES) is subdivided across its own core branches,\n // weighted by each branch's node count — mirrors how ring roots themselves share the full circle.\n const coreHierarchyByRootId = useMemo(() => {\n const map = new Map<\n string,\n { hierarchy: d3.HierarchyNode<GenreTreeNode>; angle: number; span: number }[]\n >();\n groups.forEach((group) => {\n const { coreBranches } = splitRootGroupBySide(group);\n if (coreBranches.length === 0) return;\n const rootAngle = rootAngleByRootId.get(group.root.id)!;\n const wedgeSpan = wedgeSpanForRoot(group.root.id);\n const slices = subdivideWedge(\n rootAngle,\n wedgeSpan,\n coreBranches.map((branch) => branch.nodes.length),\n );\n map.set(\n group.root.id,\n coreBranches.map((branch, branchIndex) => ({\n hierarchy: buildCoreHierarchy(d3, branch.nodes),\n angle: slices[branchIndex]!.centerDeg,\n span: slices[branchIndex]!.spanDeg,\n })),\n );\n });\n return map;\n }, [groups, rootAngleByRootId, wedgeSpanForRoot]);\n\n // Floor every ring root sits on, driven purely by chip clearance — the base every root's\n // core wedge measures outward from, before the deepest developed subtree's extent is folded in.\n const chipClearanceFloor = useMemo(\n () =>\n calculateWheelRadiusForAngles(\n layout.map((slot) => slot.angle),\n MAX_NODE_WIDTH,\n WHEEL_RADIUS,\n ),\n [layout],\n );\n\n // How far past the ring roots' own circle the deepest developed root's subtree reaches —\n // computed as a delta (base radius 0) since the actual base (wheelRadius) isn't known yet;\n // folded into wheelRadius below, then re-applied at the real base for rendering.\n const maxCoreExtentDelta = useMemo(() => {\n let extent = 0;\n coreHierarchyByRootId.forEach((branches) => {\n branches.forEach(({ hierarchy }) => {\n extent = Math.max(\n extent,\n calculateCoreSubtreeRadialExtent(\n hierarchy,\n POP_TREE_DEPTH_RADIAL_SPACING,\n 0,\n ),\n );\n });\n });\n return extent;\n }, [coreHierarchyByRootId]);\n\n const wheelRadius = useMemo(\n () => Math.max(chipClearanceFloor, chipClearanceFloor + maxCoreExtentDelta),\n [chipClearanceFloor, maxCoreExtentDelta],\n );\n\n // Read via a ref rather than depending on `onRootSelect` directly — consumers commonly pass an\n // inline callback, which would otherwise re-fire this effect (and any state it sets) every render.\n const onRootSelectRef = useRef(onRootSelect);\n useEffect(() => {\n onRootSelectRef.current = onRootSelect;\n });\n\n useEffect(() => {\n if (effectiveTopRootId) onRootSelectRef.current?.(effectiveTopRootId);\n }, [effectiveTopRootId]);\n\n useEffect(() => {\n if (!coreSvgRef.current) return;\n\n const svg = d3.select(coreSvgRef.current);\n svg.selectAll(\"*\").remove();\n const originGroup = svg\n .append(\"g\")\n .attr(\"transform\", `translate(${wheelRadius}, ${wheelRadius})`);\n\n coreHierarchyByRootId.forEach((branches, rootId) => {\n const rootLinkOrigin = getRadialPointOnCircle(\n rootAngleByRootId.get(rootId) ?? 0,\n wheelRadius,\n );\n\n const sectorGroup = originGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-core-sector\")\n .attr(\"data-gtv-root-id\", rootId);\n\n branches.forEach(({ hierarchy, angle, span }, branchIndex) => {\n const laidOut = computeCoreRadialLayout(\n d3,\n hierarchy,\n angle,\n span,\n wheelRadius,\n POP_TREE_DEPTH_RADIAL_SPACING,\n );\n const reparentForbiddenIds = reparentingNodeId\n ? (laidOut\n .descendants()\n .find((d) => d.data.id === reparentingNodeId)\n ?.descendants()\n .map((d) => d.data.id) ?? [])\n : [];\n\n const branchGroup = sectorGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-core-branch\")\n .attr(\"data-gtv-branch-index\", branchIndex);\n\n renderPopSubtree(\n d3,\n branchGroup,\n laidOut,\n getGenreTreeColor(index.byId.get(rootId)?.wikidataId),\n reparentingNodeId,\n reparentForbiddenIds,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId)\n void onReparent?.(reparentingNodeId, newParentId);\n },\n onNodeClick: (data, event) => {\n const clickedElement = event.currentTarget as Element | null;\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n additionalActions,\n playingNodeId,\n playState,\n },\n wheelItemCountRange,\n {\n isCoreSector: true,\n rootLinkOrigin,\n selectedNodeId: panel?.node.id ?? null,\n },\n );\n });\n });\n }, [\n index.byId,\n coreHierarchyByRootId,\n wheelRadius,\n rootAngleByRootId,\n reparentingNodeId,\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n wheelItemCountRange,\n panel?.node.id,\n ]);\n\n // Starts the view fit to the wheel + rendered subtrees instead of at scale 1 / pan (0, 0) —\n // guarded so it only fires once anchored content has actually rendered, and never again\n // afterward so it doesn't fight the user's own pan/zoom on later selections.\n const hasInitialFitRef = useRef(false);\n useEffect(() => {\n if (hasInitialFitRef.current) return;\n const elements = [wheelCircleRef.current, coreSvgRef.current];\n if (!elements.some(Boolean)) return;\n hasInitialFitRef.current = true;\n panZoom.fitToFrame(elements, { silent: true });\n });\n\n const handleChipClick = (rootId: string) => {\n setTopRootId(rootId);\n };\n\n // Every root's core subtree is always mounted (see the SVG-building effect above), so an\n // externally-controlled selection (e.g. from a search result) never needs a root swap — just\n // locate its element and open the panel exactly as a direct click would.\n useEffect(() => {\n if (!selectedNodeId || panel?.node.id === selectedNodeId) return;\n const targetNode = index.byId.get(selectedNodeId);\n const element = viewportRef.current?.querySelector(\n `#group-${CSS.escape(selectedNodeId)}`,\n );\n if (!targetNode || !element) return;\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(targetNode, element, viewportRef.current, { notify: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps -- panZoom re-created on pan/zoom; guarded by the panel?.node.id check above\n }, [selectedNodeId, index, panel, showNodeInfo]);\n\n // One divider per boundary between two angularly-adjacent roots — each root's own continuous\n // (unwrapped) angle plus half its weight-proportional width (sectorSpanByRootId) lands exactly on\n // the boundary with its next neighbor, since computeRadialLayout tiled the roots' widths\n // contiguously around the circle in the first place.\n const dividerAngles = useMemo(() => {\n if (groups.length <= 1) return [];\n return groups.map((group) => {\n const angle = continuousAngleByRootId.get(group.root.id) ?? 0;\n const width = sectorSpanByRootId.get(group.root.id) ?? 0;\n return angle + width / 2;\n });\n }, [groups, continuousAngleByRootId, sectorSpanByRootId]);\n\n // One tinted sector fan per root, bounded by its own weight-proportional width rather than\n // bisected neighbor angles — see computeSectorWidths's doc comment for why bisection\n // under/over-shoots a root's true sector when its neighbors' weights differ a lot from its own.\n const sectorFills = useMemo(() => {\n if (groups.length <= 1) return [];\n return groups.map((group) => {\n const angle = continuousAngleByRootId.get(group.root.id) ?? 0;\n const width = sectorSpanByRootId.get(group.root.id) ?? 0;\n return {\n rootId: group.root.id,\n start: angle - width / 2,\n clipPath: buildSectorClipPathPolygon(width),\n color: hexToRgba(\n getGenreTreeColor(group.root.wikidataId),\n ROOT_SECTOR_FILL_OPACITY,\n ),\n };\n });\n }, [groups, continuousAngleByRootId, sectorSpanByRootId]);\n\n return (\n <div\n ref={viewportRef}\n aria-busy={busy}\n className={[\n \"gtv-wheel-container\",\n \"gtv-wheel-container--radial\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n \"--gtv-wheel-radius\": `${wheelRadius}px`,\n \"--gtv-wheel-rotation-transition-ms\": `${WHEEL_ROTATION_TRANSITION_MS}ms`,\n \"--gtv-wheel-rotation-easing\": WHEEL_ROTATION_EASING,\n touchAction: panZoom.touchAction,\n } as React.CSSProperties\n }\n onPointerDown={panZoom.handlePointerDown}\n >\n {/* height: 100% (not just width) so this is the containing block the radial stage's own\n top/left: 50% measure against — see the equivalent comment on GenreTreeWheelBase. */}\n <div\n ref={stageRef}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n transform: panZoom.transform,\n transformOrigin: \"0 0\",\n }}\n >\n <div className=\"gtv-wheel-stage\">\n <div className=\"gtv-wheel-circle\" ref={wheelCircleRef} />\n\n {/* Sector wash sits in its own .gtv-wheel layer, rendered before the node svg below, so\n the wash paints under the developed nodes instead of dulling them — see the second\n .gtv-wheel layer (after the svg) for the dividers/chips, which stay above the nodes. */}\n <div className=\"gtv-wheel\">\n {sectorFills.map((sector) => (\n <div\n key={`sector-${sector.rootId}`}\n className=\"gtv-wheel-sector\"\n style={\n {\n \"--gtv-sector-angle\": `${sector.start}deg`,\n \"--gtv-sector-color\": sector.color,\n clipPath: sector.clipPath,\n } as React.CSSProperties\n }\n />\n ))}\n </div>\n\n <svg\n ref={coreSvgRef}\n className=\"gtv-wheel-pop-layer\"\n width={wheelRadius * 2}\n height={wheelRadius * 2}\n />\n\n {centerLabel && (\n <div className=\"gtv-wheel-center-label\">{centerLabel}</div>\n )}\n\n <div className=\"gtv-wheel\">\n {dividerAngles.map((angle, index) => (\n <div\n key={`divider-${groups[index].root.id}`}\n className=\"gtv-wheel-divider\"\n style={\n {\n \"--gtv-divider-angle\": `${angle}deg`,\n } as React.CSSProperties\n }\n />\n ))}\n\n {groups.map((group, index) => {\n const slot = layout[index];\n const angle =\n continuousAngleByRootId.get(group.root.id) ?? slot?.angle ?? 0;\n const chipColor = getGenreTreeColor(group.root.wikidataId);\n const rootItemCount = rootItemCountById.get(group.root.id)!;\n const dimensions = calculateNodeDimensions(\n rootItemCount,\n rootItemCountRange,\n );\n const fontSize = calculateNodeFontSize(\n rootItemCount,\n rootItemCountRange,\n );\n return (\n <div\n key={group.root.id}\n className=\"gtv-wheel-slot\"\n style={\n { \"--gtv-chip-angle\": `${angle}deg` } as React.CSSProperties\n }\n >\n <div className=\"gtv-wheel-chip-anchor\">\n <button\n type=\"button\"\n id={`group-${group.root.id}`}\n className=\"gtv-wheel-chip gtv-wheel-chip--selected\"\n style={\n {\n width: dimensions.WIDTH,\n // Height comes from --gtv-wheel-chip-base-height (see .gtv-wheel-chip in\n // styles.css), not an inline `height`/`minHeight` — an inline `height` always\n // wins the cascade over the CSS class's `height: calc(...)` hover override\n // (inline style beats any stylesheet rule), silently pinning the box and\n // breaking the hover growth again, same failure mode as before.\n \"--gtv-wheel-chip-base-height\": `${dimensions.HEIGHT}px`,\n \"--gtv-chip-color\": chipColor,\n } as React.CSSProperties\n }\n onClick={(event) => {\n panZoom.centerOnElement(\n event.currentTarget,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(event.currentTarget, viewportRef.current, INFO_PANEL_WIDTH),\n );\n handleChipClick(group.root.id);\n if (!reparentingNodeId) {\n showNodeInfo(\n group.root,\n event.currentTarget,\n viewportRef.current,\n );\n onNodeClick?.(group.root, event.nativeEvent);\n }\n }}\n >\n {PER_TREE_ACCENT_DOT && (\n <span className=\"gtv-wheel-chip-dot\" />\n )}\n <span\n className=\"gtv-node-label gtv-node-label--root\"\n style={{ fontSize }}\n >\n {group.root.name}\n </span>\n </button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {panel && (\n <InfoPanel\n node={panel.node}\n fill={rootColorOf(panel.node)}\n textColor={ACCENT_TEXT_COLOR}\n parentNode={(() => {\n const parent = index.byId.get(panel.node.parentId ?? \"\");\n return parent\n ? {\n node: parent,\n fill: rootColorOf(parent),\n textColor: ACCENT_TEXT_COLOR,\n }\n : null;\n })()}\n // renderPopSubtree is always called here with isCoreSector: true (no pop/core split in\n // this renderer), so every node gets a solid rootColor fill.\n childNodes={(index.childrenByParentId.get(panel.node.id) ?? []).map(\n (n) => ({ node: n, fill: rootColorOf(n), textColor: ACCENT_TEXT_COLOR }),\n )}\n ancestorNodes={index.ancestorsOf(panel.node.id).map(\n (n): InfoPanelChild => ({\n node: n,\n fill: rootColorOf(n),\n textColor: ACCENT_TEXT_COLOR,\n }),\n )}\n side={panel.side}\n onClose={closeNodeInfo}\n onSelectNode={(id) => {\n const targetNode = index.byId.get(id)!;\n const element = viewportRef.current!.querySelector(\n `#group-${CSS.escape(id)}`,\n );\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(\n targetNode,\n element ?? viewportRef.current,\n viewportRef.current,\n );\n }}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n renderInfoPanelBody={renderInfoPanelBody}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n }\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n\n <FloatingControls\n panZoom={panZoom}\n orientation={controlsOrientation}\n leading={floatingControlsLeading}\n busy={busy}\n onFit={() => panZoom.fitToFrame([wheelCircleRef.current, coreSvgRef.current])}\n />\n </div>\n );\n}\n","import { DOUBLE_CLICK_DELAY_MS } from \"./constants\";\n\n/** Runs a click's action only once no second click follows, so the pan/zoom glide it starts can't\n * move the target out from under the cursor before `dblclick` fires. */\nexport function createDeferredClick() {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n const cancel = () => {\n if (timeoutId !== null) clearTimeout(timeoutId);\n timeoutId = null;\n };\n const schedule = (action: () => void) => {\n cancel();\n timeoutId = setTimeout(() => {\n timeoutId = null;\n action();\n }, DOUBLE_CLICK_DELAY_MS);\n };\n return { schedule, cancel };\n}\n","export interface RadialSlot {\n rootIndex: number;\n angle: number;\n}\n\n/** Full radial layout for `weights.length` roots (fixed ring order) with `clickedIndex` landing at\n * `landingAngle` (CSS `rotate()` convention: 0 = top, clockwise; 90 = right, this wheel's\n * confirmed landing direction). Every root gets an arc proportional to its own `weights` entry\n * (its subtree's node count) rather than an equal `360/rootCount` share — a root with more nodes\n * occupies proportionally more of the circle. Ring order is unchanged: roots are still laid out\n * in order starting at the clicked root, walking the ring from `clickedIndex`. Each root's angle\n * is the center of its own proportionally-sized arc, so the clicked root's arc is centered exactly\n * on `landingAngle`, matching the even-spacing case's guarantee that the clicked root's own angle\n * equals `landingAngle`. Every root is fully developed. Returns one entry per ring index\n * (0..weights.length-1), indexable by the same index groupNodesByRoot's output uses. Empty for\n * `weights.length <= 0`. Every weight must be positive (a root's own weight is its subtree's node\n * count, which always includes the root itself, so a zero/negative weight indicates invalid\n * input rather than a real, reachable state) — throws otherwise rather than silently coercing it. */\nexport function computeRadialLayout(weights: number[], clickedIndex: number, landingAngle: number): RadialSlot[] {\n const rootCount = weights.length;\n if (rootCount <= 0) return [];\n\n if (weights.some((weight) => weight <= 0)) {\n throw new Error(\"computeRadialLayout: every root weight (node count) must be positive\");\n }\n\n const total = weights.reduce((sum, weight) => sum + weight, 0);\n const orderedWidths = weights.map((_, i) => (weights[(clickedIndex + i) % rootCount] / total) * 360);\n\n const slots: RadialSlot[] = new Array(rootCount);\n let boundary = landingAngle - orderedWidths[0] / 2;\n for (let i = 0; i < rootCount; i++) {\n const width = orderedWidths[i];\n const rootIndex = (clickedIndex + i) % rootCount;\n slots[rootIndex] = { rootIndex, angle: (((boundary + width / 2) % 360) + 360) % 360 };\n boundary += width;\n }\n\n return slots;\n}\n\n/** Each root's true angular sector width in degrees, proportional to its own `weights` entry out\n * of the total — the same per-root share `computeRadialLayout` uses internally to place chips, but\n * exposed directly so callers reconstructing a root's sector bounds (dividers, colored wash, wedge\n * caps) can derive them from the actual weights instead of bisecting neighboring roots' angles\n * (which only approximates the true bounds, and badly so when neighbors' weights differ a lot from\n * the root's own — see computeSectorBounds). */\nexport function computeSectorWidths(weights: number[]): number[] {\n const total = weights.reduce((sum, weight) => sum + weight, 0);\n return weights.map((weight) => (weight / total) * 360);\n}\n\n/** Splits a wedge (centered on `wedgeCenterDeg`, spanning `wedgeSpanDeg`) into `weights.length`\n * sub-wedges, each proportional to its own weight — same proportional-width logic as\n * `computeSectorWidths`, applied within a wedge instead of the full 360deg circle. A wedge, unlike\n * the full ring, never wraps past 360, so sub-wedges are simply laid out left to right from the\n * wedge's own start angle. Returns each sub-wedge's own center angle and span, in the same order\n * as `weights`. Empty for `weights.length <= 0`. */\nexport function subdivideWedge(\n wedgeCenterDeg: number,\n wedgeSpanDeg: number,\n weights: number[],\n): { centerDeg: number; spanDeg: number }[] {\n if (weights.length <= 0) return [];\n\n const total = weights.reduce((sum, weight) => sum + weight, 0);\n const spans = weights.map((weight) => (weight / total) * wedgeSpanDeg);\n\n const slices: { centerDeg: number; spanDeg: number }[] = [];\n let boundary = wedgeCenterDeg - wedgeSpanDeg / 2;\n for (const spanDeg of spans) {\n slices.push({ centerDeg: boundary + spanDeg / 2, spanDeg });\n boundary += spanDeg;\n }\n return slices;\n}\n\n/** Midpoint angle between two chip angles that may be continuous/unwrapped (e.g. from\n * computeContinuousAngles), taking the shorter arc between them rather than always going the\n * increasing-angle way — brings `b` within 180 degrees of `a` before averaging, so the divider\n * sits between the two chips visually instead of on the far side of the circle. Used to place a\n * radial divider on the boundary between two angularly-adjacent roots. */\nexport function bisectAngles(a: number, b: number): number {\n const diff = b - a;\n const adjustedB = diff > 180 ? b - 360 : diff < -180 ? b + 360 : b;\n return (a + adjustedB) / 2;\n}\n\n/** `clip-path: polygon(...)` value for a filled circular-sector fan spanning `[0, sweepDeg]`,\n * measured clockwise from local \"up\" (CSS `rotate()` convention) — meant for a huge square element\n * centered on the wheel's pivot (same oversized-element + `.gtv-wheel-container` overflow: hidden\n * clipping trick as `.gtv-wheel-divider`), then rotated to its sector's own start angle via\n * `transform: rotate()`, same as a divider line. Interpolates a point every `stepDeg` (default 10)\n * around the arc rather than just the two end rays: a naive 3-point triangle (center + two far\n * points) chord-clips away the middle of any sector wider than ~180 degrees, which every root gets\n * when there are only 2. Radius 50% (of the element's own box) reaches that huge element's edge,\n * same as the box being exactly 2x oversized in every direction from center. Empty string (no\n * shape / nothing rendered) for a non-positive sweep. */\nexport function buildSectorClipPathPolygon(sweepDeg: number, stepDeg = 10): string {\n if (sweepDeg <= 0) return \"\";\n const points: string[] = [\"50% 50%\"];\n const steps = Math.max(1, Math.ceil(sweepDeg / stepDeg));\n for (let i = 0; i <= steps; i++) {\n const theta = (Math.min(i * stepDeg, sweepDeg) * Math.PI) / 180;\n const x = 50 + 50 * Math.sin(theta);\n const y = 50 - 50 * Math.cos(theta);\n points.push(`${x}% ${y}%`);\n }\n return `polygon(${points.join(\", \")})`;\n}\n\n/** Start/end angle (continuous, CSS `rotate()` convention — may exceed 360 or go negative, same as\n * `continuousAngleByRootId`) of the sector belonging to the root at `index` in `continuousAngles`\n * (ring order). Both bounds are computed anchored on that root's own continuous angle (`bisectAngles(mine, neighbor)`,\n * not the reverse) so they land in the same unwrapped frame as each other regardless of how far\n * `continuousAngleByRootId` has wound up over prior re-layouts — `end - start` is always the\n * sector's true sweep instead of an arbitrary multiple of 360 off, unlike naively differencing two\n * independently-anchored `dividerAngles` entries.\n *\n * n === 2 is special-cased: with only one other root, `prev` and `next` are the same root, so the\n * general formula above degenerates to a zero-width sector (both bounds land on the same\n * shortest-arc bisection). There, the single boundary between the two roots and its opposite point\n * (+/- 180) are each root's two bounds — same as the two (coincident) entries dividerAngles\n * produces for n === 2, just split into the two 180-degree halves they actually imply. */\nexport function computeSectorBounds(\n continuousAngles: number[],\n index: number,\n): { start: number; end: number } {\n const n = continuousAngles.length;\n const mine = continuousAngles[index];\n\n if (n === 2) {\n const other = continuousAngles[(index + 1) % 2];\n const boundary = bisectAngles(mine, other);\n return mine < boundary ? { start: boundary - 180, end: boundary } : { start: boundary, end: boundary + 180 };\n }\n\n const prev = continuousAngles[(index - 1 + n) % n];\n const next = continuousAngles[(index + 1) % n];\n return { start: bisectAngles(mine, prev), end: bisectAngles(mine, next) };\n}\n\n/** Widest symmetric wedge, centered on the root's own angle (`continuousAngles[index]`), that still\n * fits entirely within that root's actual angular sector (see `computeSectorBounds`) — twice the\n * smaller of the two distances from the root's own angle to each of its sector's bounds. A sector\n * isn't generally symmetric around the root's own angle (neighbors can sit at uneven angular\n * distances, e.g. when `computeRadialLayout` gives roots weight-proportional arc widths), so capping\n * a symmetric wedge at the raw sector width (`end - start`) lets the wider side spill past its\n * boundary into the neighboring sector even though the narrower side has no slack to give. */\nexport function computeSectorSymmetricSpan(continuousAngles: number[], index: number): number {\n const mine = continuousAngles[index];\n const { start, end } = computeSectorBounds(continuousAngles, index);\n return 2 * Math.min(mine - start, end - mine);\n}\n\n/** Generalizes calculateWheelRadius's chord-overlap guarantee (see wheel-geometry.ts) to\n * non-uniformly-spaced angles: the ring only needs to grow enough to clear the SMALLEST adjacent\n * gap actually present (including the wraparound gap from the last angle back to the first), not\n * a uniform 360/n gap. Falls back to baseRadius when there are fewer than 2 angles, since a\n * single chip has no neighbor to overlap. */\nexport function calculateWheelRadiusForAngles(angles: number[], maxChipWidth: number, baseRadius: number): number {\n if (angles.length < 2) return baseRadius;\n\n const sorted = [...angles].sort((a, b) => a - b);\n let minGapDeg = 360;\n for (let i = 0; i < sorted.length; i++) {\n const next = i < sorted.length - 1 ? sorted[i + 1] : sorted[0] + 360;\n minGapDeg = Math.min(minGapDeg, next - sorted[i]);\n }\n if (minGapDeg <= 0) return baseRadius;\n\n const CHIP_GAP = 16;\n const requiredRadius = (maxChipWidth + CHIP_GAP) / (2 * Math.sin((minGapDeg * Math.PI) / 360));\n return Math.max(baseRadius, requiredRadius);\n}\n\n/** Smallest radius ≥ `floorRadius` at which `angleNeededAt(radius)` (rad, non-increasing in\n * radius) fits the full circle — binary-searched since what a ring needs depends on its radius. */\nexport function solveRingRadius(angleNeededAt: (radius: number) => number, floorRadius: number): number {\n const fits = (radius: number) => angleNeededAt(radius) <= 2 * Math.PI;\n if (fits(floorRadius)) return floorRadius;\n let lo = floorRadius;\n let hi = floorRadius * 2;\n while (!fits(hi)) {\n lo = hi;\n hi *= 2;\n }\n while (hi - lo > 1) {\n const mid = (lo + hi) / 2;\n if (fits(mid)) hi = mid;\n else lo = mid;\n }\n return hi;\n}\n\nexport interface Box {\n x0: number;\n y0: number;\n x1: number;\n y1: number;\n}\n\nexport interface LabelCullItem {\n id: string;\n /** Candidate label boxes, in preference order. */\n labels: Box[];\n dot: Box;\n priority: number;\n /** Drawn even when no candidate is free: falls back to the first one and still blocks later labels. */\n forced?: boolean;\n}\n\nconst boxesOverlap = (a: Box, b: Box) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;\n\nconst GRID_CELL = 64;\n\n/** Uniform-grid index over boxes: `some` only visits boxes sharing a cell with the query box. */\nfunction createBoxGrid<T>() {\n const cells = new Map<string, { box: Box; value: T }[]>();\n const cellRange = (box: Box) => ({\n cx0: Math.floor(box.x0 / GRID_CELL),\n cx1: Math.floor(box.x1 / GRID_CELL),\n cy0: Math.floor(box.y0 / GRID_CELL),\n cy1: Math.floor(box.y1 / GRID_CELL),\n });\n return {\n insert(box: Box, value: T) {\n const { cx0, cx1, cy0, cy1 } = cellRange(box);\n for (let cx = cx0; cx <= cx1; cx++) {\n for (let cy = cy0; cy <= cy1; cy++) {\n const key = `${cx},${cy}`;\n const bucket = cells.get(key);\n if (bucket) bucket.push({ box, value });\n else cells.set(key, [{ box, value }]);\n }\n }\n },\n some(box: Box, predicate: (value: T) => boolean) {\n const { cx0, cx1, cy0, cy1 } = cellRange(box);\n for (let cx = cx0; cx <= cx1; cx++) {\n for (let cy = cy0; cy <= cy1; cy++) {\n if (cells.get(`${cx},${cy}`)?.some((entry) => boxesOverlap(box, entry.box) && predicate(entry.value))) {\n return true;\n }\n }\n }\n return false;\n },\n };\n}\n\n/** Index of each item's chosen label candidate, or -1 when it must be hidden: greedily, by\n * descending priority, takes each item's first candidate that overlaps no already-placed label and\n * no other node's dot (a `forced` item falls back to its first candidate instead of hiding). */\nexport function placeLabels(items: LabelCullItem[]): Map<string, number> {\n const placement = new Map<string, number>();\n const keptGrid = createBoxGrid<true>();\n const dotGrid = createBoxGrid<string>();\n for (const item of items) dotGrid.insert(item.dot, item.id);\n for (const item of [...items].sort((a, b) => b.priority - a.priority)) {\n let index = item.labels.findIndex(\n (label) => !keptGrid.some(label, () => true) && !dotGrid.some(label, (id) => id !== item.id),\n );\n if (index < 0 && item.forced && item.labels.length > 0) index = 0;\n placement.set(item.id, index);\n if (index >= 0) keptGrid.insert(item.labels[index], true);\n }\n return placement;\n}\n\n/** Id of the sector (degrees, `center ± span / 2`, any winding) containing `angle`, or null. */\nexport function findSectorAtAngle(\n angle: number,\n sectors: { id: string; center: number; span: number }[],\n): string | null {\n const sector = sectors.find(\n ({ center, span }) => Math.abs(((((angle - center) % 360) + 540) % 360) - 180) <= span / 2,\n );\n return sector?.id ?? null;\n}\n\nconst boxGap = (a: Box, b: Box) =>\n Math.hypot(Math.max(0, a.x0 - b.x1, b.x0 - a.x1), Math.max(0, a.y0 - b.y1, b.y0 - a.y1));\n\n/** Center of a `width` x `height` box inside `area` that overlaps no obstacle, sampled on a grid:\n * spots passing `prefer` first, then the widest gap to the nearest obstacle (capped at `height`, so\n * \"clear enough\" ties), then the one closest to the area's center. Null when every spot is blocked. */\n// ponytail: fixed 9x9 sampling grid, sample finer if labels visibly miss obvious gaps.\nexport function pickEmptyLabelSpot(\n area: Box,\n width: number,\n height: number,\n obstacles: Box[],\n prefer: (x: number, y: number) => boolean = () => true,\n): { x: number; y: number } | null {\n const STEPS = 9;\n const spanX = area.x1 - area.x0 - width;\n const spanY = area.y1 - area.y0 - height;\n if (spanX < 0 || spanY < 0) return null;\n const centerX = (area.x0 + area.x1) / 2;\n const centerY = (area.y0 + area.y1) / 2;\n let best: { x: number; y: number; score: [number, number, number] } | null = null;\n for (let i = 0; i < STEPS; i++) {\n for (let j = 0; j < STEPS; j++) {\n const x = area.x0 + width / 2 + (spanX * i) / (STEPS - 1);\n const y = area.y0 + height / 2 + (spanY * j) / (STEPS - 1);\n const box = { x0: x - width / 2, x1: x + width / 2, y0: y - height / 2, y1: y + height / 2 };\n if (obstacles.some((obstacle) => boxesOverlap(box, obstacle))) continue;\n const gap = Math.min(height, ...obstacles.map((obstacle) => boxGap(box, obstacle)));\n const score: [number, number, number] = [\n prefer(x, y) ? 1 : 0,\n gap,\n -Math.hypot(x - centerX, y - centerY),\n ];\n const k = best ? score.findIndex((value, index) => value !== best!.score[index]) : 0;\n if (!best || (k >= 0 && score[k] > best.score[k])) best = { x, y, score };\n }\n }\n return best && { x: best.x, y: best.y };\n}\n","import * as d3 from \"d3\";\n\nimport { createDeferredClick } from \"./defer-click\";\nimport { GenreTreeAction, GenreTreeNode, GenreTreePlayState } from \"./types\";\nimport {\n addReparentTargetOverlay,\n buildTreeHierarchyStructure,\n} from \"./NodeHelper\";\nimport {\n ACCENT_COLOR,\n ItemCountRange,\n MAX_NODE_WIDTH,\n NODE_DOT_LABEL_GAP,\n NODE_LAYOUT_SLOT_RADIUS,\n POP_SECTOR_TINT_RATIO,\n POP_TREE_DEPTH_RADIAL_SPACING,\n RADIAL_CORE_LINK_TINT_RATIO,\n RADIAL_POP_LINK_TINT_RATIO,\n SURFACE_BORDER_COLOR,\n TEXT_COLOR,\n calculateRootLabelFontSize,\n TEXT_MUTED_COLOR,\n calculateLinkWidth,\n calculateNodeBorderWidth,\n calculateNodeDotRadius,\n calculateSelectedNodeBorderWidth,\n calculateLabelFontSizeAtZoom,\n calculateLabelRank,\n calculateNodeFontSize,\n isLabelRankedForZoom,\n tintSurface,\n} from \"./constants\";\nimport {\n Box,\n placeLabels,\n} from \"./radial-wheel-geometry\";\n\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\ntype D3Selection = d3.Selection<SVGGElement, unknown, null, undefined>;\n\n// Angular width of the wedge a pop subtree fans out into, centered on its own root's direction —\n// narrower than the root's own full sector so it never touches a neighboring root's wedge,\n// matching the gutter every other radial-wheel element keeps from its neighbors.\nexport const POP_WEDGE_SPAN_DEGREES = 80;\n\n// Margin (px) added past a pop subtree's deepest node's own half-width, so its rendered card\n// never sits flush against the wheel's own circle edge.\nexport const POP_SUBTREE_OUTER_MARGIN = 24;\n\n// A pop hierarchy (per buildPopHierarchy/splitRootGroupBySide) is rooted at the pop child, which\n// is always the ring root's direct child — i.e. always absolute depth 1 in the whole tree,\n// regardless of which root it belongs to.\nconst POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH = 1;\nconst DOT_RADIUS_TRANSITION_MS = 250;\n\n/** The radius (px, from the wheel's true center) for a node at `depth` steps below the ring roots'\n * own circle (`coreRootCircleRadius`) — the single formula every radial depth (ring roots, their\n * pop branches, and the center \"Mainstream Pop\" subtree) shares, so that any two nodes at the same\n * absolute depth always land on the same circle regardless of which branch/subtree they belong to. */\nexport function getRadialDepthRadius(\n depth: number,\n coreRootCircleRadius: number,\n depthSpacing: number,\n): number {\n return coreRootCircleRadius + depth * depthSpacing;\n}\n\n/** Builds the pop subtree's hierarchy, rooted at the pop child (root not included) — thin wrapper\n * over buildTreeHierarchyStructure so callers of this module don't need to import NodeHelper\n * directly just for this one call. */\n// splitRootGroupBySide (pop-core-split.ts) returns the pop subtree rooted at the pop child, whose\n// own parentId still points at the (excluded) root — d3.stratify requires every parentId to\n// either be null or resolve within the given set, so that dangling reference is normalized to\n// null here, making the pop child stratify's root. Each resulting hierarchy node's `.data` is then\n// restored to its original (un-normalized) node so the real parentId still reaches\n// renderers/click handlers/the info panel.\nexport function buildPopHierarchy(\n d3Lib: typeof import(\"d3\"),\n popNodes: GenreTreeNode[],\n): D3Node {\n const ids = new Set(popNodes.map((node) => node.id));\n const byId = new Map(popNodes.map((node) => [node.id, node]));\n const normalized = popNodes.map((node) =>\n node.parentId !== null && !ids.has(node.parentId)\n ? { ...node, parentId: null }\n : node,\n );\n const hierarchy = buildTreeHierarchyStructure(d3Lib, normalized);\n hierarchy.each((d) => {\n d.data = byId.get(d.data.id) ?? d.data;\n });\n return hierarchy;\n}\n\n/** The outer radius (px, from `coreRootCircleRadius`) a pop subtree needs to render without\n * overlapping the wheel's own edge — driven by its deepest node's ring (absolute depth\n * `POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH + height`) plus that node's own half-width and a fixed outer\n * margin. Used to grow the wheel's circle to fit whichever developed root's pop subtree\n * reaches furthest.\n *\n * `coreRootCircleRadius` shifts the whole subtree outward by that amount, mirroring\n * `computePopRadialLayout`'s own base radius — pass 0 to get the extent as a delta past the ring\n * roots' own circle (e.g. when that circle's own size is still being determined from this delta),\n * or the wheel's actual `coreRootCircleRadius` to get the subtree's true outer radius. */\nexport function calculatePopSubtreeRadialExtent(\n hierarchy: D3Node,\n coreRootCircleRadius = 0,\n): number {\n return (\n getRadialDepthRadius(\n POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH + hierarchy.height,\n coreRootCircleRadius,\n POP_TREE_DEPTH_RADIAL_SPACING,\n ) +\n MAX_NODE_WIDTH / 2 +\n POP_SUBTREE_OUTER_MARGIN\n );\n}\n\n/**\n * Lays a pop subtree's hierarchy out in polar coordinates within a wedge centered on\n * `wedgeCenterAngleDegrees` (CSS `rotate()` convention: 0 = top, clockwise — same convention as\n * radial-wheel-geometry.ts's angles), then projects each node onto cartesian (x, y) relative to\n * the wheel's own center — writes the result directly onto each node's `.x`/`.y`, mirroring how\n * tree-renderer.ts's setupTreeLayout overwrites a d3 hierarchy's laid-out coordinates in place.\n *\n * Angle spread across siblings/cousins uses d3's tidy-tree balancing (`d3.tree()`), with each\n * neighbour gap measured from the two dots' real arc length on their ring (see `measureWedge`). Radius is NOT taken from that layout's own y — every node's radius climbs INWARD from\n * `coreRootCircleRadius` (the ring roots' own circle, where this branch's ring root chip sits) by\n * `POP_TREE_DEPTH_RADIAL_SPACING` per depth step, so the branch's own root (absolute depth\n * `POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH`) always lands exactly one depth step inside that circle\n * regardless of how deep the branch grows — anchoring outward from the mainstream circle instead\n * would leave shallower branches stranded partway to the ring roots' circle while only the single\n * deepest developed branch actually reached it. Any slack this leaves between the branch's own\n * deepest node and the mainstream circle is absorbed there, not by moving the branch's own root\n * off the ring roots' circle.\n *\n * `wedgeSpanDegrees` defaults to `POP_WEDGE_SPAN_DEGREES`; the wheel passes each branch's slice of\n * its root's sector, sized from `measureWedge`; the layout fills it up to `MAX_WEDGE_STRETCH` (see layoutWedge).\n *\n * `depthSpacing` defaults to `POP_TREE_DEPTH_RADIAL_SPACING` — kept constant per depth step\n * regardless of how far out the wheel's own outer circle ends up sitting; it's the outer circle's\n * own radius (`coreRootCircleRadius`, sized off the deepest developed pop branch's reach — see\n * `popReachRequiredRadius` in the wheel component) that adapts to fit the pop branches, not the\n * spacing between their depths. */\nexport function computePopRadialLayout(\n d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n wedgeCenterAngleDegrees: number,\n coreRootCircleRadius: number,\n wedgeSpanDegrees: number = POP_WEDGE_SPAN_DEGREES,\n depthSpacing: number = POP_TREE_DEPTH_RADIAL_SPACING,\n): D3Node {\n return layoutWedge(\n d3Lib,\n hierarchy,\n wedgeCenterAngleDegrees,\n wedgeSpanDegrees,\n popRadiusOfDepth(coreRootCircleRadius, depthSpacing),\n );\n}\n\nexport const MIN_DOT_GAP = 5.6;\n\n/** Radius of a pop hierarchy's node at `depth`, climbing inward from the ring roots' circle. */\nexport function popRadiusOfDepth(coreRootCircleRadius: number, depthSpacing: number) {\n return (depth: number) =>\n getRadialDepthRadius(-(POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH + depth), coreRootCircleRadius, depthSpacing);\n}\n\n/** Radius of a core hierarchy's node at `depth`, fanning outward from the ring roots' circle. */\nexport function coreRadiusOfDepth(coreRootCircleRadius: number, depthSpacing: number) {\n return (depth: number) => getRadialDepthRadius(depth + 1, coreRootCircleRadius, depthSpacing);\n}\n\ntype ContourEdge = { x: number; dotRadius: number };\ntype Contour = { left: ContourEdge; right: ContourEdge }[];\n\n/** Lays `hierarchy` out angularly with each pair of same-depth neighbours just far enough apart for\n * their dots not to overlap on their own ring — a deeper (pop) ring is smaller, so its nodes need\n * more angle each. Leaves each node's `.x` as its angle (rad) from the wedge's leading edge and\n * returns the wedge's minimal span (rad). Without `itemCountRange` every dot is assumed max size.\n *\n * Packs each node's children left to right, each one shifted just far enough to clear the\n * accumulated contour of its earlier siblings, parent centered over its first and last child. Not\n * d3.tree: its Reingold–Tilford pass spreads the push between two wide subtrees evenly across every\n * small sibling between them, which on deep rings leaves leaf siblings far apart. */\nexport function measureWedge(\n _d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n radiusOfDepth: (depth: number) => number,\n): number {\n const dotRadius = () => NODE_LAYOUT_SLOT_RADIUS;\n const offsetFromParent = new Map<D3Node, number>();\n const shiftLevel = (level: Contour[number], by: number) => ({\n left: { ...level.left, x: level.left.x + by },\n right: { ...level.right, x: level.right.x + by },\n });\n\n const pack = (node: D3Node): Contour => {\n const own = { x: 0, dotRadius: dotRadius() };\n if (!node.children) return [{ left: own, right: own }];\n const below: Contour = [];\n const childOffsets: number[] = [];\n for (const child of node.children) {\n const contour = pack(child);\n let offset = 0;\n if (childOffsets.length > 0) {\n offset = -Infinity;\n for (let k = 0; k < Math.min(below.length, contour.length); k++) {\n const gap =\n (below[k].right.dotRadius + contour[k].left.dotRadius + MIN_DOT_GAP) / radiusOfDepth(child.depth + k);\n offset = Math.max(offset, below[k].right.x - contour[k].left.x + gap);\n }\n }\n childOffsets.push(offset);\n contour.forEach((level, k) => {\n const shifted = shiftLevel(level, offset);\n if (k < below.length) below[k].right = shifted.right;\n else below.push(shifted);\n });\n }\n const center = (childOffsets[0] + childOffsets[childOffsets.length - 1]) / 2;\n node.children.forEach((child, i) => offsetFromParent.set(child, childOffsets[i] - center));\n return [{ left: own, right: own }, ...below.map((level) => shiftLevel(level, -center))];\n };\n\n pack(hierarchy);\n hierarchy.eachBefore((d) => {\n d.x = d.parent ? d.parent.x! + offsetFromParent.get(d)! : 0;\n });\n let min = Infinity;\n let max = -Infinity;\n hierarchy.each((d) => {\n const inset = (dotRadius() + MIN_DOT_GAP / 2) / radiusOfDepth(d.depth);\n min = Math.min(min, d.x! - inset);\n max = Math.max(max, d.x! + inset);\n });\n hierarchy.each((d) => {\n d.x = d.x! - min;\n });\n return max - min;\n}\n\n// Cap on how far a wedge's measured layout may stretch to use slack in its slice: a uniform angular\n// stretch grows outer rings' arc gaps with their radius, so stretching to fill pushes deep siblings\n// far apart. Unused slack stays as empty gutter around the centered layout.\nexport const MAX_WEDGE_STRETCH = 1.42;\n\n/** What `layoutWedge` does with the angle left over once the layout is stretched to\n * `MAX_WEDGE_STRETCH`: `\"center\"` leaves it as an empty gutter on both sides of the wedge;\n * `\"between-children\"` spreads it into even gaps between the first branching node's children, so\n * the layout reaches both edges of its wedge. */\nexport type WedgeSlack = \"center\" | \"between-children\";\n\nfunction splitAtBranching(hierarchy: D3Node): { chain: D3Node[]; branching: D3Node; children: D3Node[] } | null {\n let branching: D3Node = hierarchy;\n const chain: D3Node[] = [];\n while (branching.children?.length === 1) {\n chain.push(branching);\n branching = branching.children[0];\n }\n const children = branching.children;\n return children && children.length > 1 ? { chain, branching, children } : null;\n}\n\n/** Measures `hierarchy` (see measureWedge), squeezes it to fit `wedgeSpanDegrees` or stretches it\n * by at most `MAX_WEDGE_STRETCH`, places it per `slack`, and projects each node to cartesian x/y\n * around the wheel's center at its depth's radius. */\nexport function layoutWedge(\n d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n wedgeCenterAngleDegrees: number,\n wedgeSpanDegrees: number,\n radiusOfDepth: (depth: number) => number,\n slack: WedgeSlack = \"center\",\n): D3Node {\n const neededRad = measureWedge(d3Lib, hierarchy, radiusOfDepth);\n const wedgeSpanRad = (wedgeSpanDegrees * Math.PI) / 180;\n const scale = Math.min(MAX_WEDGE_STRETCH, wedgeSpanRad / neededRad);\n const wedgeCenterRad = (wedgeCenterAngleDegrees * Math.PI) / 180;\n const centeredStartRad = wedgeCenterRad - (neededRad * scale) / 2;\n\n const branchingSplit = slack === \"between-children\" ? splitAtBranching(hierarchy) : null;\n if (!branchingSplit) {\n hierarchy.each((d) => {\n const angleRad = centeredStartRad + d.x! * scale;\n const radius = radiusOfDepth(d.depth);\n d.x = radius * Math.sin(angleRad);\n d.y = -radius * Math.cos(angleRad);\n });\n return hierarchy;\n }\n\n const { chain, branching, children } = branchingSplit;\n const gap = Math.max(0, wedgeSpanRad - neededRad * scale) / (children.length - 1);\n const wedgeStartRad = wedgeCenterRad - wedgeSpanRad / 2;\n const angleOffsetRad = new Map<D3Node, number>();\n hierarchy.each((d) => angleOffsetRad.set(d, d.x! * scale));\n children.forEach((child, i) => {\n child.each((d) => angleOffsetRad.set(d, angleOffsetRad.get(d)! + i * gap));\n });\n const recenter = (gap * (children.length - 1)) / 2;\n [...chain, branching].forEach((d) => angleOffsetRad.set(d, angleOffsetRad.get(d)! + recenter));\n\n hierarchy.each((d) => {\n const angleRad = wedgeStartRad + angleOffsetRad.get(d)!;\n const radius = radiusOfDepth(d.depth);\n d.x = radius * Math.sin(angleRad);\n d.y = -radius * Math.cos(angleRad);\n });\n return hierarchy;\n}\n\n/**\n * Lays out the center \"Mainstream Pop\" node's own subtree in polar coordinates, full-circle\n * (unlike `computePopRadialLayout`'s 80deg wedge — the center node has no single ring-root\n * direction to anchor to). `hierarchy` must be rooted at the center node itself: depth 0 (the\n * center node) is pinned to the wheel's true origin and excluded from angular layout; depth 1 (its\n * direct children) land exactly one `depthSpacing` step past `coreRootCircleRadius` — the same\n * circle every ring root's pop hierarchy's absolute-depth-1 node lands on too, so the center\n * subtree's rings coincide with the ring roots' own absolute depth circles — spread around the\n * full circle proportional to each child's own subtree size (d3.tree's default separation, unlike\n * the wedge layout's measured one); each deeper generation steps outward by\n * `depthSpacing`.\n */\nexport function computeCenterRadialLayout(\n d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n coreRootCircleRadius: number,\n depthSpacing: number,\n): D3Node {\n const treeLayout = d3Lib.tree<GenreTreeNode>().size([2 * Math.PI, 1]);\n treeLayout(hierarchy);\n\n hierarchy.each((d) => {\n if (d.depth === 0) {\n d.x = 0;\n d.y = 0;\n return;\n }\n const angleRad = d.x!;\n const radius = getRadialDepthRadius(\n d.depth,\n coreRootCircleRadius,\n depthSpacing,\n );\n d.x = radius * Math.sin(angleRad);\n d.y = -radius * Math.cos(angleRad);\n });\n\n return hierarchy;\n}\n\n/** The \"mainstream pop outer circle\" radius (px, from the wheel's center) the center \"Mainstream\n * Pop\" node's own subtree needs to render without overlapping the wheel's own edge (the \"core\n * root circle\") — driven by its deepest node's ring plus that node's own half-width and a fixed\n * outer margin. Only meaningful when the center node actually has children (`hierarchy.height >=\n * 1`). */\nexport function calculateMainstreamPopOuterCircleRadius(\n hierarchy: D3Node,\n coreRootCircleRadius: number,\n depthSpacing: number,\n): number {\n return (\n getRadialDepthRadius(hierarchy.height, coreRootCircleRadius, depthSpacing) +\n MAX_NODE_WIDTH / 2 +\n POP_SUBTREE_OUTER_MARGIN\n );\n}\n\n/** Cartesian (x, y) position on the circle of the given radius, at the given angle in the CSS\n * `rotate()` convention (0 = top, clockwise) — the same convention `computePopRadialLayout` and\n * `computeCoreRadialLayout` project their own nodes with, and the one `.gtv-wheel-slot`'s\n * `rotate(...) translateY(-radius)` (styles.css) positions the root's own JSX-rendered chip with.\n * Used to find where that chip sits in the D3 hierarchy's coordinate space, so the root->depth1\n * link can be drawn even though the root itself isn't part of the hierarchy. */\nexport function getRadialPointOnCircle(\n angleDegrees: number,\n radius: number,\n): { x: number; y: number } {\n const angleRad = (angleDegrees * Math.PI) / 180;\n return { x: radius * Math.sin(angleRad), y: -radius * Math.cos(angleRad) };\n}\n\n// Average glyph advance as a fraction of font size — a DOM-free label width estimate, so layout\n// and collision culling stay deterministic (happy-dom has no getBBox).\nexport const LABEL_CHAR_WIDTH_RATIO = 0.6;\n\nexport interface DotLabelCandidate {\n x: number;\n y: number;\n anchor: \"start\" | \"middle\" | \"end\";\n box: Box;\n}\n\n/** A radial dot node's label placement and boxes, local to the node's own center. `candidates` are\n * the label's possible spots in preference order (see hideCollidingLabels): beside the dot facing\n * away from the wheel's vertical axis (right of the dot when `x >= 0`, left otherwise, so it never\n * reads across the wheel's center), then the other side, then centered above, then below.\n * `labelX`/`anchor`/`label` are the first candidate. With `zoomScale`, the font size is the one that\n * renders at the label's fixed on-screen size under that zoom (see calculateLabelFontSizeAtZoom). */\nexport function getDotLabelGeometry(\n x: number,\n name: string,\n itemCount: number,\n itemCountRange: ItemCountRange,\n zoomScale?: number,\n): {\n dotRadius: number;\n fontSize: number;\n labelX: number;\n anchor: \"start\" | \"end\";\n label: Box;\n dot: Box;\n hitArea: Box;\n candidates: DotLabelCandidate[];\n} {\n const dotRadius = calculateNodeDotRadius(itemCount, itemCountRange);\n const fontSize =\n zoomScale === undefined\n ? calculateNodeFontSize(itemCount, itemCountRange)\n : calculateLabelFontSizeAtZoom(itemCount, itemCountRange, zoomScale);\n const candidates = getLabelCandidates(x, name, dotRadius, fontSize);\n const { x: labelX, box: label } = candidates[0];\n const dot = { x0: -dotRadius, x1: dotRadius, y0: -dotRadius, y1: dotRadius };\n const halfHeight = Math.max(dotRadius, fontSize / 2);\n return {\n dotRadius,\n fontSize,\n labelX,\n anchor: x >= 0 ? \"start\" : \"end\",\n label,\n dot,\n hitArea: {\n x0: Math.min(dot.x0, label.x0),\n x1: Math.max(dot.x1, label.x1),\n y0: -halfHeight,\n y1: halfHeight,\n },\n candidates,\n };\n}\n\nfunction getLabelCandidates(x: number, name: string, dotRadius: number, fontSize: number): DotLabelCandidate[] {\n const width = name.length * fontSize * LABEL_CHAR_WIDTH_RATIO;\n const halfFont = fontSize / 2;\n const side = (right: boolean): DotLabelCandidate => {\n const labelX = (right ? 1 : -1) * (dotRadius + NODE_DOT_LABEL_GAP);\n return {\n x: labelX,\n y: 0,\n anchor: right ? \"start\" : \"end\",\n box: { x0: right ? labelX : labelX - width, x1: right ? labelX + width : labelX, y0: -halfFont, y1: halfFont },\n };\n };\n const stacked = (below: boolean): DotLabelCandidate => {\n const y = (below ? 1 : -1) * (dotRadius + NODE_DOT_LABEL_GAP + halfFont);\n return { x: 0, y, anchor: \"middle\", box: { x0: -width / 2, x1: width / 2, y0: y - halfFont, y1: y + halfFont } };\n };\n const right = x >= 0;\n return [side(right), side(!right), stacked(false), stacked(true)];\n}\n\n/** A root chip's label, drawn in the SVG (see renderRootLabels) so it shares the dot labels' look\n * and collision placement. `x`/`y` are the chip's center, `itemCount` its subtree total. */\nexport interface RootLabelDatum {\n id: string;\n name: string;\n x: number;\n y: number;\n radius: number;\n itemCount: number;\n /** Named even when no candidate spot is free (the wheel's center); other roots hide like dot labels. */\n alwaysShown?: boolean;\n}\n\nexport function renderRootLabels<E extends SVGElement>(\n container: d3.Selection<E, unknown, null, undefined>,\n roots: RootLabelDatum[],\n): void {\n container\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-root-labels\")\n .selectAll<SVGGElement, RootLabelDatum>(\"g.gtv-root-label\")\n .data(roots)\n .join(\"g\")\n .attr(\"class\", \"gtv-root-label\")\n .attr(\"transform\", (d) => `translate(${d.x}, ${d.y})`)\n .append(\"text\")\n .attr(\"class\", \"gtv-node-label\")\n .attr(\"dominant-baseline\", \"middle\")\n .attr(\"fill\", TEXT_COLOR)\n .text((d) => d.name);\n}\n\nexport interface RenderPopSubtreeCallbacks {\n onPlayPause?: (nodeId: string) => void;\n onAddChild?: (parentId: string) => void;\n onRenameRequest?: (node: GenreTreeNode) => void;\n onDeleteRequest?: (node: GenreTreeNode) => void;\n onReparentRequest?: (node: GenreTreeNode) => void;\n onReparentTargetSelect: (newParentId: string) => void;\n onNodeClick?: (node: GenreTreeNode, event: MouseEvent, element: Element) => void;\n onNodeDoubleClick?: (node: GenreTreeNode) => void;\n onNodeHover?: (node: GenreTreeNode) => void;\n additionalActions?: (node: GenreTreeNode) => GenreTreeAction[];\n playingNodeId?: string | null;\n playState?: GenreTreePlayState;\n}\n\n/**\n * Renders an already polar-laid-out pop subtree (see computePopRadialLayout) into `svg`, a `<g>`\n * already translated to the wheel's own center — every node's `.x`/`.y` is that node's true\n * rendered center, unlike tree-renderer.ts's cartesian nodes (which are each node's top-left slot\n * corner, offset by `+ width / 2` at render time). Card drawing, hover toolbar, and reparent-target\n * overlay reuse the exact same NodeHelper/d3-path-helper building blocks tree-renderer.ts's\n * renderTree uses, so pop nodes look and behave identically to every other node in the package —\n * only the positioning math and link shape (radial curves following the wheel's own rings, not\n * tree-renderer's orthogonal links) are specific to this renderer.\n */\nexport function renderPopSubtree(\n d3Lib: typeof import(\"d3\"),\n svg: D3Selection,\n hierarchy: D3Node,\n rootColor: string,\n reparentingNodeId: string | null,\n reparentForbiddenIds: string[],\n callbacks: RenderPopSubtreeCallbacks,\n itemCountRange: ItemCountRange,\n options: {\n skipRootNode?: boolean;\n // Core nodes continue the root chip's own solid-color style (matches every other renderer's\n // treatment of a root); pop nodes keep the lighter tint that sets the pop wedge apart from core.\n isCoreSector?: boolean;\n // Where the hierarchy's own root (depth 0) connects to, when that root ISN'T part of the\n // hierarchy but instead rendered separately as its own JSX wheel chip (every per-root core/pop\n // subtree — buildCoreHierarchy/buildPopHierarchy both exclude the ring root itself, unlike the\n // center \"Mainstream Pop\" subtree, whose hierarchy still includes its own root at depth 0). When\n // given, one extra gtv-link is drawn per depth-0 node from this point to that node, since\n // hierarchy.links() has no edge for it (a hierarchy root has no incoming link).\n rootLinkOrigin?: { x: number; y: number };\n // The center \"Mainstream Pop\" subtree is white: it keeps dark label/toolbar text, grey links and\n // black dot borders; every other pop sector reads light against its tinted fill instead,\n // matching core sectors' solid-color treatment.\n isMainstreamSector?: boolean;\n // The node whose info panel is currently open (see use-node-info-panel.ts) — gets a heavier\n // accent-colored border while every other node in the subtree dims.\n selectedNodeId?: string | null;\n // Per-node fill overriding the sector-wide one derived from rootColor, for a caller whose node\n // colors must not depend on which sector they happen to render in.\n nodeFillOf?: (node: GenreTreeNode) => string;\n // Last rendered dot radius per node id, read to animate a size change (e.g. the size-metric\n // toggle) from the old radius and overwritten with the new ones.\n dotRadiusMemory?: Map<string, number>;\n } = {},\n): void {\n const {\n skipRootNode = false,\n isCoreSector = false,\n rootLinkOrigin,\n isMainstreamSector = false,\n selectedNodeId = null,\n nodeFillOf,\n dotRadiusMemory,\n } = options;\n const { onReparentTargetSelect, onNodeClick, onNodeDoubleClick, onNodeHover } = callbacks;\n const isForbidden = (d: D3Node) => reparentForbiddenIds.includes(d.data.id);\n const isSelected = (d: D3Node) => d.data.id === selectedNodeId;\n // A node is \"related\" to the selected node when it's the selected node's own parent or one of\n // its children — these dim less than the rest of the tree.\n const isRelatedNode = (d: D3Node) =>\n d.parent?.data.id === selectedNodeId ||\n (d.children ?? []).some((c) => c.data.id === selectedNodeId);\n const sectorFill = isCoreSector\n ? rootColor\n : tintSurface(rootColor, POP_SECTOR_TINT_RATIO);\n const nodeFill = (d: D3Node) => nodeFillOf?.(d.data) ?? sectorFill;\n // The center's white rootColor would tint its links and dot borders to white, invisible on the\n // white wheel.\n const linkColor = isMainstreamSector\n ? TEXT_MUTED_COLOR\n : tintSurface(\n rootColor,\n isCoreSector ? RADIAL_CORE_LINK_TINT_RATIO : RADIAL_POP_LINK_TINT_RATIO,\n );\n // skipRootNode omits the hierarchy's own depth-0 node from the drawn cards — used for the center\n // \"Pop\" node's subtree, whose depth-0 node already renders as its own dedicated wheel chip.\n const drawnNodes = skipRootNode\n ? hierarchy.descendants().filter((d) => d.depth > 0)\n : hierarchy.descendants();\n const rootLinks = rootLinkOrigin\n ? hierarchy\n .descendants()\n .filter((d) => d.depth === 0)\n .map((d) => ({\n source: rootLinkOrigin,\n target: { x: d.x!, y: d.y! },\n sourceId: null,\n targetId: d.data.id,\n targetItemCount: d.data.itemCount,\n }))\n : [];\n const links: {\n source: { x?: number; y?: number };\n target: { x?: number; y?: number };\n sourceId: string | null;\n targetId: string | null;\n targetItemCount: number;\n }[] = [\n ...hierarchy.links().map((d) => ({\n source: d.source,\n target: d.target,\n sourceId: d.source.data.id,\n targetId: d.target.data.id,\n targetItemCount: d.target.data.itemCount,\n })),\n ...rootLinks,\n ];\n // A link is \"related\" to the selected node when the selected node is one of its two endpoints\n // (its parent link, or a link to one of its children) — every other link in this subtree dims.\n const isRelatedLink = (d: (typeof links)[number]) =>\n d.sourceId === selectedNodeId || d.targetId === selectedNodeId;\n\n // Cartesian (x, y) here is always a point on a circle centered on the wheel's own center (see\n // computePopRadialLayout/computeCenterRadialLayout), so it's exactly invertible back to the\n // (angle, radius) polar pair d3.linkRadial expects — recovering that lets links curve smoothly\n // along the wheel's rings instead of cutting straight chords across them.\n const toPolar = (p: { x?: number; y?: number }) => {\n const x = p.x ?? 0;\n const y = p.y ?? 0;\n return { x: Math.atan2(x, -y), y: Math.hypot(x, y) };\n };\n const radialLinkGenerator = d3Lib\n .linkRadial<\n { source: { x: number; y: number }; target: { x: number; y: number } },\n { x: number; y: number }\n >()\n .angle((d) => d.x)\n .radius((d) => d.y);\n\n svg\n .selectAll(\"path.gtv-link\")\n .data(links)\n .enter()\n .append(\"path\")\n .attr(\n \"class\",\n (d) =>\n \"gtv-link\" +\n (selectedNodeId && !isRelatedLink(d) ? \" gtv-link--dimmed\" : \"\"),\n )\n .attr(\"d\", (d) =>\n radialLinkGenerator({\n source: toPolar(d.source),\n target: toPolar(d.target),\n }),\n )\n .style(\"fill\", \"none\")\n .style(\"stroke\", linkColor)\n .style(\"stroke-width\", (d) =>\n calculateLinkWidth(d.targetItemCount, itemCountRange),\n )\n .style(\"stroke-linecap\", \"round\");\n\n const nodes = svg\n .selectAll<SVGGElement, D3Node>(\"g.node\")\n .data(drawnNodes)\n .enter()\n .append(\"g\")\n .attr(\n \"class\",\n (d) =>\n \"node\" +\n (isForbidden(d) ? \" gtv-node--forbidden\" : \"\") +\n (selectedNodeId && !isSelected(d)\n ? isRelatedNode(d)\n ? \" gtv-node--dimmed-related\"\n : \" gtv-node--dimmed\"\n : \"\"),\n )\n .attr(\"id\", (d) => \"group-\" + d.data.id)\n // Levels below the nearest root chip (1 = its direct child), for hideCollidingLabels' ranking:\n // with skipRootNode, depth 0 is that chip itself.\n .attr(\"data-gtv-depth\", (d) => d.depth + (skipRootNode ? 0 : 1))\n .attr(\"transform\", (d) => `translate(${d.x}, ${d.y})`)\n .style(\"--gtv-node-fill\", nodeFill);\n\n nodes\n .append(\"rect\")\n .attr(\"class\", \"gtv-hover-hit-area\")\n .each(function (d) {\n const { hitArea } = getDotLabelGeometry(\n d.x!,\n d.data.name,\n d.data.itemCount,\n itemCountRange,\n );\n d3Lib\n .select(this)\n .attr(\"x\", hitArea.x0)\n .attr(\"y\", hitArea.y0)\n .attr(\"width\", hitArea.x1 - hitArea.x0)\n .attr(\"height\", hitArea.y1 - hitArea.y0);\n })\n .attr(\"fill\", \"transparent\")\n .attr(\"pointer-events\", \"all\");\n\n const targetDotRadius = (d: D3Node) => calculateNodeDotRadius(d.data.itemCount, itemCountRange);\n const animateDots =\n dotRadiusMemory !== undefined &&\n !(typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches);\n const startDotRadius = (d: D3Node) =>\n (animateDots ? dotRadiusMemory?.get(d.data.id) : undefined) ?? targetDotRadius(d);\n\n nodes\n .append(\"circle\")\n .attr(\"class\", \"gtv-node-dot\")\n .attr(\"r\", startDotRadius)\n .attr(\"fill\", nodeFill)\n .attr(\"stroke\", (d) =>\n isSelected(d)\n ? ACCENT_COLOR\n : isMainstreamSector\n ? TEXT_COLOR\n : SURFACE_BORDER_COLOR,\n )\n .attr(\"stroke-width\", (d) =>\n isSelected(d)\n ? calculateSelectedNodeBorderWidth(calculateNodeDotRadius(d.data.itemCount, itemCountRange))\n : calculateNodeBorderWidth(d.data.itemCount, itemCountRange),\n )\n .style(\"pointer-events\", \"none\")\n .filter((d) => startDotRadius(d) !== targetDotRadius(d))\n .transition()\n .duration(DOT_RADIUS_TRANSITION_MS)\n .ease(d3Lib.easeCubicOut)\n .attr(\"r\", targetDotRadius);\n\n nodes.each((d) => dotRadiusMemory?.set(d.data.id, targetDotRadius(d)));\n\n // Smallest dots painted last so a big dot never hides a small one it overlaps.\n nodes.sort((a, b) => targetDotRadius(b) - targetDotRadius(a));\n\n nodes\n .append(\"text\")\n .attr(\n \"class\",\n \"gtv-node-label\",\n )\n .each(function (d) {\n const { labelX, anchor, fontSize } = getDotLabelGeometry(\n d.x!,\n d.data.name,\n d.data.itemCount,\n itemCountRange,\n );\n d3Lib\n .select(this)\n .attr(\"x\", labelX)\n .attr(\"text-anchor\", anchor)\n .style(\"font-size\", `${fontSize}px`);\n })\n .attr(\"dominant-baseline\", \"middle\")\n .attr(\"fill\", TEXT_COLOR)\n // Opts back in past .gtv-wheel-pop-layer's inherited pointer-events: none — the hit area is\n // sized for the label's initial spot, and hideCollidingLabels may move the label elsewhere.\n .attr(\"pointer-events\", \"all\")\n .text((d) => d.data.name);\n\n nodes.each(function (d: D3Node) {\n const group = d3Lib.select<SVGGElement, unknown>(this);\n let leaveTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n group.on(\"mouseenter\", function () {\n if (leaveTimeoutId !== null) {\n clearTimeout(leaveTimeoutId);\n leaveTimeoutId = null;\n }\n\n if (reparentingNodeId && d.data.actionable !== false && !isForbidden(d)) {\n addReparentTargetOverlay(\n d3Lib,\n group as unknown as d3.Selection<\n SVGGElement,\n unknown,\n HTMLElement,\n unknown\n >,\n onReparentTargetSelect,\n itemCountRange,\n );\n }\n });\n\n group.on(\"pointerenter\", () => onNodeHover?.(d.data));\n\n group.on(\"mouseleave\", function () {\n leaveTimeoutId = setTimeout(() => {\n leaveTimeoutId = null;\n d3Lib\n .select<\n SVGGElement,\n unknown\n >(\"#select-as-new-parent-group-\" + d.data.id)\n .remove();\n }, 100);\n });\n\n // Toolbar/menu buttons stopPropagation, so this only fires for a click that actually landed\n // on the card body itself (the reparent-target overlay doesn't stopPropagation, but its click\n // only exists while reparentingNodeId is set, which the guard below already suppresses).\n // See tree-renderer.ts's identical handler.\n const deferredClick = createDeferredClick();\n group.on(\"click\", (event: MouseEvent) => {\n if (reparentingNodeId) return;\n const element = event.currentTarget as Element;\n deferredClick.schedule(() => onNodeClick?.(d.data, event, element));\n });\n\n group.on(\"dblclick\", () => {\n deferredClick.cancel();\n onNodeDoubleClick?.(d.data);\n });\n });\n}\n\n/** Places every dot label under `container` (across all rendered subtrees, so labels of\n * neighboring branches/sectors are checked against each other) on its first candidate spot (see\n * getDotLabelGeometry) that overlaps no higher-ranked label (see calculateLabelRank) and no dot, else marks it\n * `gtv-node-label--hidden` (CSS reveals it again on hover). Labels below the zoom's importance\n * threshold (see isLabelRankedForZoom) are hidden outright. Labels are sized for `zoomScale`: zoomed\n * out they are bigger in layout units and collide more, so fewer of them show. */\nexport function hideCollidingLabels<E extends SVGElement>(\n d3Lib: typeof import(\"d3\"),\n container: d3.Selection<E, unknown, null, undefined>,\n itemCountRange: ItemCountRange,\n zoomScale: number,\n): void {\n const nodes = container.selectAll<SVGGElement, D3Node>(\"g.node\");\n const offset = (box: Box, x: number, y: number): Box => ({\n x0: box.x0 + x,\n x1: box.x1 + x,\n y0: box.y0 + y,\n y1: box.y1 + y,\n });\n const geometryById = new Map(\n nodes\n .data()\n .map((d) => [\n d.data.id,\n getDotLabelGeometry(d.x!, d.data.name, d.data.itemCount, itemCountRange, zoomScale),\n ]),\n );\n const roots = container.selectAll<SVGGElement, RootLabelDatum>(\"g.gtv-root-label\");\n const rootGeometryById = new Map(\n roots.data().map((d) => {\n const fontSize = calculateRootLabelFontSize(\n calculateLabelFontSizeAtZoom(d.itemCount, itemCountRange, zoomScale),\n zoomScale,\n );\n return [d.id, { fontSize, candidates: getLabelCandidates(d.x, d.name, d.radius, fontSize) }];\n }),\n );\n const placement = placeLabels([\n ...roots.data().map((d) => ({\n id: `root:${d.id}`,\n labels: rootGeometryById.get(d.id)!.candidates.map((candidate) => offset(candidate.box, d.x, d.y)),\n dot: { x0: d.x - d.radius, x1: d.x + d.radius, y0: d.y - d.radius, y1: d.y + d.radius },\n // Ring roots outrank every dot label (ranks are <= 1), bigger roots first.\n priority: d.alwaysShown ? Infinity : 1 + calculateLabelRank(d.itemCount, itemCountRange, 1),\n forced: d.alwaysShown,\n })),\n ...nodes.nodes().map((element) => {\n const d = d3Lib.select<SVGGElement, D3Node>(element).datum();\n const { candidates, dot } = geometryById.get(d.data.id)!;\n const rank = calculateLabelRank(d.data.itemCount, itemCountRange, Number(element.dataset.gtvDepth));\n return {\n id: d.data.id,\n // An unranked node still keeps its dot as an obstacle for every other label.\n labels: isLabelRankedForZoom(rank, zoomScale)\n ? candidates.map((candidate) => offset(candidate.box, d.x!, d.y!))\n : [],\n dot: offset(dot, d.x!, d.y!),\n priority: rank,\n };\n }),\n ]);\n roots.each(function (d) {\n const { candidates, fontSize } = rootGeometryById.get(d.id)!;\n const index = placement.get(`root:${d.id}`)!;\n const candidate = candidates[Math.max(index, 0)];\n d3Lib\n .select(this)\n .select(\".gtv-node-label\")\n .attr(\"x\", candidate.x)\n .attr(\"y\", candidate.y)\n .attr(\"text-anchor\", candidate.anchor)\n .style(\"font-size\", `${fontSize}px`)\n .classed(\"gtv-node-label--hidden\", index < 0 && !d.alwaysShown);\n });\n nodes.each(function (d) {\n const { candidates, fontSize } = geometryById.get(d.data.id)!;\n const index = placement.get(d.data.id) ?? -1;\n const candidate = candidates[Math.max(index, 0)];\n d3Lib\n .select(this)\n .select(\".gtv-node-label\")\n .attr(\"x\", candidate.x)\n .attr(\"y\", candidate.y)\n .attr(\"text-anchor\", candidate.anchor)\n .style(\"font-size\", `${fontSize}px`)\n .classed(\"gtv-node-label--hidden\", index < 0);\n });\n}\n","import * as d3 from \"d3\";\n\nimport { GenreTreeNode } from \"./types\";\nimport { MAX_NODE_WIDTH } from \"./constants\";\nimport { buildTreeHierarchyStructure } from \"./NodeHelper\";\nimport { coreRadiusOfDepth, getRadialDepthRadius, layoutWedge } from \"./pop-core-radial-layout\";\n\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\n\n// Margin (px) added past a core subtree's deepest node's own half-width, so its rendered card\n// never sits flush against the wheel's outer edge — mirrors POP_SUBTREE_OUTER_MARGIN.\nconst CORE_SUBTREE_OUTER_MARGIN = 24;\n\n/** Builds a root's core subtree hierarchy, rooted at the root's own first core child (root not\n * included) — mirrors buildPopHierarchy: that child's own parentId still points at the (excluded)\n * ring root, which d3.stratify would reject, so it's normalized to null here for structure-building\n * only; each resulting hierarchy node's `.data` is then restored to its original (un-normalized)\n * node so the real parentId still reaches renderers/click handlers/the info panel. */\nexport function buildCoreHierarchy(d3Lib: typeof import(\"d3\"), coreNodes: GenreTreeNode[]): D3Node {\n const ids = new Set(coreNodes.map((node) => node.id));\n const byId = new Map(coreNodes.map((node) => [node.id, node]));\n const normalized = coreNodes.map((node) =>\n node.parentId !== null && !ids.has(node.parentId) ? { ...node, parentId: null } : node,\n );\n const hierarchy = buildTreeHierarchyStructure(d3Lib, normalized);\n hierarchy.each((d) => {\n d.data = byId.get(d.data.id) ?? d.data;\n });\n return hierarchy;\n}\n\n/**\n * Lays a root's core (non-pop) subtree out in polar coordinates, fanning outward from\n * `coreRootCircleRadius` within a wedge centered on `wedgeCenterAngleDegrees` — the mirror image of\n * `computePopRadialLayout`'s inward wedge. `hierarchy` is rooted at the ring root's own core\n * child (root not included), so depth 0 (absolute depth 1) lands one `depthSpacing` step past\n * `coreRootCircleRadius`, clear of the root chip's own boundary circle rather than straddling it,\n * and each deeper generation steps further outward by `depthSpacing`, occupying a disjoint radius\n * range from any inward pop wedge in the same quadrant.\n */\nexport function computeCoreRadialLayout(\n d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n wedgeCenterAngleDegrees: number,\n wedgeSpanDegrees: number,\n coreRootCircleRadius: number,\n depthSpacing: number,\n): D3Node {\n return layoutWedge(\n d3Lib,\n hierarchy,\n wedgeCenterAngleDegrees,\n wedgeSpanDegrees,\n coreRadiusOfDepth(coreRootCircleRadius, depthSpacing),\n \"between-children\",\n );\n}\n\n/** The outer radius (px, past `coreRootCircleRadius`) a core subtree needs to render without\n * overflowing — driven by its deepest node's ring (depth = hierarchy.height, since the hierarchy is\n * rooted at absolute depth 1 already) plus that node's own half-width and a fixed outer margin.\n * Pass 0 for `coreRootCircleRadius` to get the extent as a delta past the ring roots' own circle\n * (used while that circle's own size is still being determined). */\nexport function calculateCoreSubtreeRadialExtent(\n hierarchy: D3Node,\n depthSpacing: number,\n coreRootCircleRadius = 0,\n): number {\n return (\n getRadialDepthRadius(hierarchy.height + 1, coreRootCircleRadius, depthSpacing) +\n MAX_NODE_WIDTH / 2 +\n CORE_SUBTREE_OUTER_MARGIN\n );\n}\n","import { GenreTreeRootGroup } from \"./root-grouping\";\nimport { GenreTreeNode } from \"./types\";\n\nexport interface PopCoreBranch {\n /** The direct child this branch is rooted at. */\n child: GenreTreeNode;\n /** The child and its descendants. */\n nodes: GenreTreeNode[];\n}\n\nexport interface PopCoreSplit {\n /** The root plus every core (non-pop) branch's nodes, flattened. */\n coreNodes: GenreTreeNode[];\n /** Every pop branch's nodes, flattened (root not included). Empty when the root has no pop\n * side (e.g. classical). */\n popNodes: GenreTreeNode[];\n /** One entry per direct core (non-pop) child, each paired with its own subtree. Order matches\n * the child's position among the root's direct children. Empty when the root has no core\n * children. */\n coreBranches: PopCoreBranch[];\n /** One entry per direct pop child, mirroring `coreBranches`. Empty when the root has no pop\n * children. */\n popBranches: PopCoreBranch[];\n}\n\n/**\n * Splits one root group's nodes into its core and pop branches, per the root's direct children's\n * `side` field (\"pop\" for the optional branch; unset/\"core\" for the required one).\n *\n * A root may have any number of core (non-pop) direct children and any number of pop direct\n * children — each becomes its own branch in `coreBranches`/`popBranches`. A root with no pop\n * children yields an empty `popBranches`/`popNodes`. A root with zero direct children yields\n * both `coreBranches` and `popBranches` empty (`coreNodes` is just the root).\n */\nexport function splitRootGroupBySide(group: GenreTreeRootGroup): PopCoreSplit {\n const { root, nodes } = group;\n const nodeById = new Map(nodes.map((node) => [node.id, node]));\n const directChildren = nodes.filter((node) => node.parentId === root.id);\n\n const popChildren = directChildren.filter((node) => node.side === \"pop\");\n const coreChildren = directChildren.filter((node) => node.side !== \"pop\");\n\n const childrenByParentId = new Map<string, GenreTreeNode[]>();\n for (const node of nodes) {\n if (node.parentId === null) continue;\n const siblings = childrenByParentId.get(node.parentId);\n if (siblings) siblings.push(node);\n else childrenByParentId.set(node.parentId, [node]);\n }\n\n const collectSubtree = (startId: string): GenreTreeNode[] => {\n const start = nodeById.get(startId)!;\n\n const subtree: GenreTreeNode[] = [];\n const stack = [start];\n while (stack.length > 0) {\n const current = stack.pop()!;\n subtree.push(current);\n stack.push(...(childrenByParentId.get(current.id) ?? []));\n }\n return subtree;\n };\n\n const coreBranches = coreChildren.map((child) => ({ child, nodes: collectSubtree(child.id) }));\n const popBranches = popChildren.map((child) => ({ child, nodes: collectSubtree(child.id) }));\n\n const coreNodes = [root, ...coreBranches.flatMap((branch) => branch.nodes)];\n const popNodes = popBranches.flatMap((branch) => branch.nodes);\n\n return { coreNodes, popNodes, coreBranches, popBranches };\n}\n","\"use client\";\n\nimport { WheelRadialCore } from \"./GenreTreeWheelRadialBase\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\n\nexport interface GenreTreeWheelRadialProps extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the top (just-clicked) root changes — on mount with the default selection,\n * and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. */\n centerLabel?: string;\n}\n\n/**\n * All root genres distributed around a full circle centered in its container, each root's\n * angular width proportional to its own subtree's node count, every root developed as a full\n * subtree simultaneously. Clicking any chip re-lays-out the ring so that root lands on the\n * right, recalculating every other root's angle.\n */\nexport function GenreTreeWheelRadial(props: GenreTreeWheelRadialProps) {\n return <WheelRadialCore {...props} />;\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as d3 from \"d3\";\n\nimport { createDeferredClick } from \"./defer-click\";\nimport { buildTreeHierarchyStructure } from \"./NodeHelper\";\nimport { GenreTreeRootGroup, groupNodesByRoot } from \"./root-grouping\";\nimport { useTreeIndex } from \"./use-tree-index\";\nimport { splitRootGroupBySide } from \"./pop-core-split\";\nimport {\n buildCoreHierarchy,\n calculateCoreSubtreeRadialExtent,\n computeCoreRadialLayout,\n} from \"./core-radial-layout\";\nimport {\n buildPopHierarchy,\n calculateMainstreamPopOuterCircleRadius,\n calculatePopSubtreeRadialExtent,\n computeCenterRadialLayout,\n computePopRadialLayout,\n coreRadiusOfDepth,\n getRadialPointOnCircle,\n hideCollidingLabels,\n LABEL_CHAR_WIDTH_RATIO,\n renderRootLabels,\n measureWedge,\n popRadiusOfDepth,\n POP_SUBTREE_OUTER_MARGIN,\n POP_WEDGE_SPAN_DEGREES,\n renderPopSubtree,\n type RootLabelDatum,\n} from \"./pop-core-radial-layout\";\nimport {\n buildSectorClipPathPolygon,\n computeRadialLayout,\n computeSectorWidths,\n findSectorAtAngle,\n pickEmptyLabelSpot,\n RadialSlot,\n solveRingRadius,\n subdivideWedge,\n} from \"./radial-wheel-geometry\";\nimport { FloatingControls } from \"./FloatingControls\";\nimport { usePanZoom } from \"./use-pan-zoom\";\nimport { queryTreeContentElements } from \"./zoom-pan\";\nimport { useNodeInfoPanel } from \"./use-node-info-panel\";\nimport { useFocusHistory } from \"./use-focus-history\";\nimport { WheelBreadcrumb } from \"./WheelBreadcrumb\";\nimport { resolveInfoPanelObscuredArea } from \"./info-panel-geometry\";\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\nimport { isZoomableNode, resolvePopCoreFrame } from \"./pop-core-frame\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\nimport {\n ACCENT_COLOR,\n calculateNodeDotRadius,\n calculateSelectedNodeBorderWidth,\n CENTER_NODE_NAME,\n getGenreNodeColor,\n getGenreNodeStyle,\n getItemCountRange,\n hexToRgba,\n INFO_PANEL_WIDTH,\n isMainstreamPopLineage,\n NODE_DOT_MAX_RADIUS,\n POP_TREE_DEPTH_RADIAL_SPACING,\n ROOT_SECTOR_FILL_OPACITY,\n LABEL_RELAYOUT_DEBOUNCE_MS,\n REGION_LABEL_MAX_PX,\n REGION_LABEL_MIN_PX,\n REGION_LABEL_TARGET_PX,\n NODE_LAYOUT_SLOT_RADIUS,\n WHEEL_POP_CORE_RADIUS,\n WHEEL_ROTATION_EASING,\n WHEEL_ROTATION_TRANSITION_MS,\n ZOOM_FOCUS_SCALE,\n shadeSurface,\n UNDERGROUND_DARKEN_RATIO,\n UNDERGROUND_SECTOR_FILL_OPACITY,\n SCENE_SECTOR_OUTER_RADIUS,\n} from \"./constants\";\n\n/** Why `onFocusChange` fired: the info panel's \"Zoom in\" button or a node double-click, the parent (\"back\") /\n * root-view buttons and breadcrumb, a `selectedNodeId` outside the current zoom (e.g. a search result), or a\n * `focusNodeId` that isn't zoomable (see `isZoomableNode`). */\nexport type FocusChangeReason = \"zoom-in\" | /** @deprecated no longer emitted */ \"previous\" | \"back\" | \"root\" | \"search\" | \"rejected\";\n\n/** Focus is controlled-only: pass both props or neither, since the wheel never moves its own focus. */\nexport type PopCoreFocusProps =\n | { focusNodeId?: undefined; onFocusChange?: undefined }\n | {\n /** Renders this node's subtree as its own wheel instead of the whole tree (null) — its single\n * pop child (if any) becomes the center disc, its other children the ring roots. An id that\n * isn't zoomable (see `isZoomableNode`) renders the whole tree and fires\n * `onFocusChange(null, \"rejected\")` once. */\n focusNodeId: string | null;\n /** Fired with the node to focus and why: any zoomable node from its info panel's \"Zoom in\"\n * or a double-click on it or its label (`\"zoom-in\"`), the focus node's parent (`\"back\"`), null for the whole tree (`\"root\"`),\n * the selection's top-level root — or null when that root is Mainstream Pop or the selection\n * is itself a root — when `selectedNodeId` lies outside the zoom (`\"search\"`), and null when\n * `focusNodeId` isn't zoomable (`\"rejected\"`). Omit both props to hide the zoom-in and back\n * controls and never refocus on an outside selection. */\n onFocusChange: (id: string | null, reason: FocusChangeReason) => void;\n };\n\nexport type WheelRadialPopCoreProps = Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> & {\n nodes: GenreTreeNode[];\n /** Fired whenever the top (just-clicked) root changes — on mount with the default selection,\n * and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** When false, clicking a chip still fires `onRootSelect`, but the ring stays at its current\n * rotation instead of bringing the clicked root to the landing angle. Defaults to true. */\n allowWheelRotation?: boolean;\n} & PopCoreFocusProps;\n\n// The wheel always lands the just-clicked root on the right (matches WheelRadialCore's own\n// landingAngle=90 convention) — see computeRadialLayout's doc comment for why.\nconst LANDING_ANGLE = 90;\n\n// Clearance (px) between neighbouring ring root chips along the ring.\nconst CHIP_GAP = 16;\n\nconst sum = (values: number[]) => values.reduce((total, value) => total + value, 0);\n\n// Unwraps computeRadialLayout's [0, 360)-wrapped angles into each root's own continuous\n// (possibly >360 or negative) angle, nearest to its `previous` value — see WheelRadialCore's own\n// copy of this function for why plain wrapped angles cause a wrong-direction transition.\nfunction computeContinuousAngles(\n groups: GenreTreeRootGroup[],\n layout: RadialSlot[],\n previous: Map<string, number>,\n): Map<string, number> {\n const angles = new Map<string, number>();\n groups.forEach((group, index) => {\n const rawAngle = layout[index]?.angle ?? 0;\n const previousAngle = previous.get(group.root.id);\n const laps =\n previousAngle === undefined\n ? 0\n : Math.round((previousAngle - rawAngle) / 360);\n angles.set(group.root.id, rawAngle + 360 * laps);\n });\n return angles;\n}\n\n/**\n * Same ring mechanics as `WheelRadialCore`, with two differences: every root's outward subtree\n * renders only its *core* (non-pop) branch (`side !== \"pop\"`), and — if that root also has a pop\n * branch — that pop subtree renders as a full interactive tree fanned out *inside* the wheel's own\n * circle, in the root's own sector, instead of being hidden. The circle grows past its normal\n * chip-clearance floor as needed to fit the largest pop subtree (see\n * `calculatePopSubtreeRadialExtent`).\n */\nexport function WheelRadialPopCoreCore({\n nodes,\n className,\n onRootSelect,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n onNodeHover,\n additionalActions,\n showToolbar = true,\n renderInfoPanelBody,\n onSelectedNodeChange,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n wheelZoom,\n defaultViewport,\n onViewportChange,\n controlsOrientation = \"vertical\",\n floatingControlsLeading,\n busy,\n allowWheelRotation = true,\n selectedNodeId,\n focusNodeId: requestedFocusNodeId = null,\n onFocusChange,\n}: WheelRadialPopCoreProps) {\n const index = useTreeIndex(nodes);\n // A focus the zoom buttons couldn't have produced (stale URL, deleted node, ...) renders the\n // unfocused wheel and asks the consumer to reset it, instead of letting resolvePopCoreFrame throw.\n const focusNodeId = useMemo(\n () => (requestedFocusNodeId !== null && isZoomableNode(nodes, requestedFocusNodeId) ? requestedFocusNodeId : null),\n [nodes, requestedFocusNodeId],\n );\n const isFocusRejected = requestedFocusNodeId !== null && focusNodeId === null;\n useEffect(() => {\n if (isFocusRejected) onFocusChange?.(null, \"rejected\");\n // eslint-disable-next-line react-hooks/exhaustive-deps -- report each rejected id once, not on every new callback identity\n }, [isFocusRejected, requestedFocusNodeId]);\n const { center: layoutCenterNode, centerHasPopDisc, centerNodes: centerSubtreeNodes, ringNodes } = useMemo(\n () => resolvePopCoreFrame(nodes, focusNodeId),\n [nodes, focusNodeId],\n );\n // The frame rewrites a focused view's center/ring roots to parentId: null for layout only —\n // anything handed to the consumer or the info panel gets the original node instead.\n const centerNode = index.byId.get(layoutCenterNode.id)!;\n // A parent with several pop children can't be a focus, so Root view is the only way up from there.\n\n const centerSubtreeNodeIds = useMemo(\n () => new Set(centerSubtreeNodes.map((node) => node.id)),\n [centerSubtreeNodes],\n );\n\n // computeCenterRadialLayout spreads each depth-1 child's wedge proportional to its own subtree\n // size (d3.tree's default separation) — feeding it the center node's pop-side children too would\n // let their descendant counts inflate their core siblings' neighboring wedges, so they're\n // excluded here the same way splitRootGroupBySide excludes a ring root's pop branch from its core\n // one. Unlike the ring roots, the center node's own pop branch (if any) has no dedicated wedge\n // rendering path today, so excluding it here also drops it from view entirely.\n const centerCoreSubtreeNodes = useMemo(() => {\n if (!centerHasPopDisc) return [layoutCenterNode];\n const { childrenByParentId } = index;\n const coreChildren = (childrenByParentId.get(layoutCenterNode.id) ?? []).filter(\n (child) => child.side !== \"pop\",\n );\n const subtree: GenreTreeNode[] = [layoutCenterNode];\n const stack = [...coreChildren];\n while (stack.length > 0) {\n const current = stack.pop()!;\n subtree.push(current);\n stack.push(...(childrenByParentId.get(current.id) ?? []));\n }\n return subtree;\n }, [index, layoutCenterNode, centerHasPopDisc]);\n\n const centerSubtreeHierarchy = useMemo(\n () =>\n centerCoreSubtreeNodes.length > 1\n ? buildTreeHierarchyStructure(d3, centerCoreSubtreeNodes)\n : null,\n [centerCoreSubtreeNodes],\n );\n // The center \"Mainstream Pop\" node's own subtree is always shown when it has one; without one the\n // center chip stays a plain disc.\n const isPopExpanded = centerSubtreeHierarchy != null;\n\n const groups = useMemo(() => groupNodesByRoot(ringNodes), [ringNodes]);\n\n const splitByRootId = useMemo(\n () =>\n new Map(\n groups.map((group) => [group.root.id, splitRootGroupBySide(group)]),\n ),\n [groups],\n );\n const ringRootById = useMemo(\n () => new Map(groups.flatMap((group) => group.nodes.map((node) => [node.id, group.root]))),\n [groups],\n );\n // Every chip, dot and InfoPanel header takes this, so a genre looks the same in every zoom frame.\n const getNodeVisualStyle = useCallback(\n (node: GenreTreeNode) => getGenreNodeStyle(index, node.id),\n [index],\n );\n\n const [topRootId, setTopRootId] = useState<string | null>(\n groups[0]?.root.id ?? null,\n );\n // Tracks which root the ring is actually rotated to show at the landing angle — kept separate\n // from topRootId so a click can still fire onRootSelect (via topRootId) without moving the ring\n // when allowWheelRotation is false. Only updated on click when allowWheelRotation is true, so it\n // can lag behind topRootId until the next click after rotation is re-enabled.\n const [rotationTopRootId, setRotationTopRootId] = useState<string | null>(\n groups[0]?.root.id ?? null,\n );\n const [renderedFocusNodeId, setRenderedFocusNodeId] = useState(focusNodeId);\n if (renderedFocusNodeId !== focusNodeId) {\n setRenderedFocusNodeId(focusNodeId);\n setTopRootId(groups[0]?.root.id ?? null);\n setRotationTopRootId(groups[0]?.root.id ?? null);\n }\n const viewportRef = useRef<HTMLDivElement>(null);\n const stageRef = useRef<HTMLDivElement>(null);\n const dotRadiusMemoryRef = useRef(new Map<string, number>());\n const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });\n // Shared across every mounted sector (ring roots' pop/core branches, and the center subtree) —\n // exactly one hook instance for the whole component instance, so only one panel is ever open at\n // a time regardless of which sector's node was clicked.\n const { panel: openPanel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });\n // Derived per frame, not closed by an effect: a focus change can leave the open (or selected)\n // node outside the drawn subtree, and nothing orders such an effect against the selection one.\n // The focus node itself counts: when it has a pop child that child is the drawn center, so the\n // node zoomed into from its panel isn't drawn but its panel should stay open.\n const isInFrame = (id: string) => id === focusNodeId || centerSubtreeNodeIds.has(id) || ringRootById.has(id);\n const panel = openPanel && isInFrame(openPanel.node.id) ? openPanel : null;\n const { previous: previousSelectedId, markPreviousRequested } = useFocusHistory(\n panel?.node.id ?? null,\n isInFrame,\n );\n const selectNodeFromPanel = (id: string) => {\n const targetNode = index.byId.get(id)!;\n const element = viewportRef.current!.querySelector(`#group-${CSS.escape(id)}`);\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(targetNode, element ?? viewportRef.current, viewportRef.current);\n };\n // Read via a ref inside the D3 render effect below rather than depending on `panZoom` directly —\n // its identity changes every render, which would otherwise re-run (and re-mount the whole D3\n // tree) on every pan/zoom, exactly what that effect's own dependency list is designed to avoid.\n const centerOnElementRef = useRef(panZoom.centerOnElement);\n useEffect(() => {\n centerOnElementRef.current = panZoom.centerOnElement;\n });\n const showNodeInfoRef = useRef(showNodeInfo);\n useEffect(() => {\n showNodeInfoRef.current = showNodeInfo;\n });\n // Double-click mirrors the info panel's \"Zoom in\" button.\n const chipClickRef = useRef(createDeferredClick());\n useEffect(() => {\n const deferred = chipClickRef.current;\n return deferred.cancel;\n }, []);\n const focusOnNode = (id: string) => {\n chipClickRef.current.cancel();\n if (!onFocusChange || reparentingNodeId || id === focusNodeId || !isZoomableNode(nodes, id)) return;\n closeNodeInfo();\n onFocusChange(id, \"zoom-in\");\n };\n const focusOnNodeRef = useRef(focusOnNode);\n useEffect(() => {\n focusOnNodeRef.current = focusOnNode;\n });\n // Read via a ref so a consumer's fresh closure each render doesn't re-mount the D3 tree.\n const onNodeHoverRef = useRef(onNodeHover);\n useEffect(() => {\n onNodeHoverRef.current = onNodeHover;\n });\n const { zoomScale } = panZoom;\n const zoomScaleRef = useRef(zoomScale);\n useEffect(() => {\n zoomScaleRef.current = zoomScale;\n });\n const wheelCircleRef = useRef<HTMLDivElement>(null);\n const popSvgRef = useRef<SVGSVGElement>(null);\n const centerChipRef = useRef<HTMLButtonElement>(null);\n const regionLabelRef = useRef<HTMLDivElement>(null);\n // Stage-space (pre pan/zoom) position and font size of the current root's name, so it pans and\n // scales with the tree between re-placements.\n const [regionLabel, setRegionLabel] = useState<{\n rootId: string;\n x: number;\n y: number;\n fontSize: number;\n } | null>(null);\n\n // Falls back to the first root without writing state back when the explicitly selected root\n // disappears from `nodes` — avoids a setState-in-effect cascading render for derived state.\n const effectiveTopRootId = groups.some((group) => group.root.id === topRootId)\n ? topRootId\n : (groups[0]?.root.id ?? null);\n const effectiveRotationTopRootId = groups.some(\n (group) => group.root.id === rotationTopRootId,\n )\n ? rotationTopRootId\n : (groups[0]?.root.id ?? null);\n const topIndex = Math.max(\n groups.findIndex((group) => group.root.id === effectiveRotationTopRootId),\n 0,\n );\n\n const rootItemCountById = useMemo(\n () => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),\n [groups],\n );\n\n // Pop-subtree node cards scale against the whole tree's item counts, not just their own subtree's\n // — a pop chain's few nodes can have a narrow itemCount spread that would otherwise get stretched\n // across the full size range and render wildly inconsistent card sizes next to core siblings.\n // Root counts included so root circles and node dots share one track-count scale.\n const wheelItemCountRange = useMemo(\n () =>\n getItemCountRange([\n ...nodes,\n ...[...rootItemCountById.values()].map((itemCount) => ({ itemCount })),\n ]),\n [nodes, rootItemCountById],\n );\n\n const isCenterMainstream = isMainstreamPopLineage(index, centerNode.id);\n const centerNodeColor = isCenterMainstream ? \"#ffffff\" : getGenreNodeStyle(index, centerNode.id).fill;\n const centerChipDiameter = 2 * calculateNodeDotRadius(centerNode.itemCount, wheelItemCountRange);\n\n const branchHierarchiesByRootId = useMemo(\n () =>\n new Map(\n groups.map((group) => {\n const split = splitByRootId.get(group.root.id);\n return [\n group.root.id,\n {\n pop: (split?.popBranches ?? []).map((branch) => buildPopHierarchy(d3, branch.nodes)),\n core: (split?.coreBranches ?? []).map((branch) => buildCoreHierarchy(d3, branch.nodes)),\n },\n ];\n }),\n ),\n [groups, splitByRootId],\n );\n\n // How far past the ring roots' own circle the tallest developed root's pop wedge reaches —\n // computed as a delta (base radius 0) since the actual base (coreRootCircleRadius) isn't known\n // yet; folded into coreRootCircleRadius below, then re-applied at the real base for rendering.\n const maxPopExtentDelta = useMemo(() => {\n let extent = 0;\n branchHierarchiesByRootId.forEach(({ pop }) => {\n pop.forEach((hierarchy) => {\n extent = Math.max(extent, calculatePopSubtreeRadialExtent(hierarchy, 0));\n });\n });\n return extent;\n }, [branchHierarchiesByRootId]);\n\n // The mainstream circle's radius from the wheel's true center — how far the center node's own\n // subtree reaches, or the center chip's own disc when it has none. The deepest pop node must\n // clear this, not just some fixed distance past the ring roots' own circle.\n const mainstreamCircleRadius = useMemo(\n () =>\n centerSubtreeHierarchy\n ? calculateMainstreamPopOuterCircleRadius(\n centerSubtreeHierarchy,\n 0,\n POP_TREE_DEPTH_RADIAL_SPACING,\n )\n : NODE_LAYOUT_SLOT_RADIUS,\n [centerSubtreeHierarchy],\n );\n\n // How far past the ring roots' own circle the deepest developed root's core branch reaches —\n // same idea as maxPopExtentDelta. Core branches render outward from coreRootCircleRadius\n // regardless of its value, so this doesn't belong in coreRootCircleRadius itself (that would\n // needlessly drag the visual outer circle and ring root chips outward with it) — it only feeds\n // svgCanvasRadius below, so the deepest core branch has enough SVG canvas to render into\n // without being clipped.\n const maxCoreExtentDelta = useMemo(() => {\n let extent = 0;\n branchHierarchiesByRootId.forEach(({ core }) => {\n core.forEach((hierarchy) => {\n extent = Math.max(\n extent,\n calculateCoreSubtreeRadialExtent(hierarchy, POP_TREE_DEPTH_RADIAL_SPACING, 0),\n );\n });\n });\n return extent;\n }, [branchHierarchiesByRootId]);\n\n // Pins coreRootCircleRadius so the deepest node of the deepest developed pop branch (which\n // climbs inward from coreRootCircleRadius by a fixed depthSpacing per depth step — see\n // computePopRadialLayout) has its card clear the mainstream circle by the usual half-width +\n // outer margin, rather than leaving that gap to whatever coreRootCircleRadius happens to be for\n // other reasons. Zero (dropped from the Math.max below) when no root has a pop branch at all.\n const popReachRequiredRadius = useMemo(\n () =>\n maxPopExtentDelta > 0 ? mainstreamCircleRadius + maxPopExtentDelta : 0,\n [mainstreamCircleRadius, maxPopExtentDelta],\n );\n\n // Angles (rad) each root's pop branches, core branches and chip need at ring radius `radius`.\n const measureBranchNeedsAt = useCallback(\n (radius: number) =>\n new Map(\n groups.map((group) => {\n const { pop, core } = branchHierarchiesByRootId.get(group.root.id)!;\n const chipWidth = 2 * NODE_LAYOUT_SLOT_RADIUS;\n return [\n group.root.id,\n {\n pop: pop.map((hierarchy) =>\n measureWedge(\n d3,\n hierarchy,\n popRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),\n ),\n ),\n core: core.map((hierarchy) =>\n measureWedge(\n d3,\n hierarchy,\n coreRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),\n ),\n ),\n chip: 2 * Math.asin(Math.min(1, (chipWidth / 2 + CHIP_GAP) / radius)),\n },\n ];\n }),\n ),\n [groups, branchHierarchiesByRootId],\n );\n\n // The visual outer circle's radius (--gtv-wheel-radius) and, equally, where every ring root's\n // own chip sits — NOT the SVG canvas size (see svgCanvasRadius below). The smallest radius at\n // which every root's measured need fits the full circle, floored so ring root chips clear the\n // mainstream circle and the deepest pop branch clears it too. Deliberately excludes\n // maxCoreExtentDelta: a deep core branch needs canvas room, not a bigger ring.\n const coreRootCircleRadius = useMemo(\n () =>\n solveRingRadius(\n (radius) =>\n [...measureBranchNeedsAt(radius).values()].reduce(\n (total, needs) => total + Math.max(sum(needs.pop), sum(needs.core), needs.chip),\n 0,\n ),\n Math.max(\n WHEEL_POP_CORE_RADIUS,\n mainstreamCircleRadius + NODE_DOT_MAX_RADIUS + POP_SUBTREE_OUTER_MARGIN,\n popReachRequiredRadius,\n ),\n ),\n [measureBranchNeedsAt, mainstreamCircleRadius, popReachRequiredRadius],\n );\n\n const branchNeedsByRootId = useMemo(\n () => measureBranchNeedsAt(coreRootCircleRadius),\n [measureBranchNeedsAt, coreRootCircleRadius],\n );\n\n // Each root's sector is the angle it measurably needs at the solved ring radius — the widest of\n // its pop wedge, its core wedge (disjoint radius bands) and its own chip — so slack is shared in\n // proportion to real need rather than node count.\n const rootWeights = useMemo(\n () =>\n groups.map((group) => {\n const needs = branchNeedsByRootId.get(group.root.id)!;\n return Math.max(sum(needs.pop), sum(needs.core), needs.chip);\n }),\n [groups, branchNeedsByRootId],\n );\n\n const layout = useMemo(\n () => computeRadialLayout(rootWeights, topIndex, LANDING_ANGLE),\n [rootWeights, topIndex],\n );\n\n // computeRadialLayout always returns angles wrapped to [0, 360) — re-rendering with a fresh\n // wrapped value on every selection change means a chip landing back near 0deg (top) has to\n // transition from e.g. 270deg down through 90/0, i.e. always counterclockwise, regardless of\n // which way the ring conceptually just turned. Track each chip's own last displayed (unwrapped)\n // angle and add/subtract full turns so its CSS transition always takes its own shortest path.\n const [angleMemo, setAngleMemo] = useState(() => ({\n layout,\n angles: computeContinuousAngles(groups, layout, new Map()),\n }));\n if (angleMemo.layout !== layout) {\n setAngleMemo({\n layout,\n angles: computeContinuousAngles(groups, layout, angleMemo.angles),\n });\n }\n const continuousAngleByRootId = angleMemo.angles;\n\n // Real angular sector each ring root owns, proportional to its own weight (rootWeights) out of\n // the total — same widths computeRadialLayout placed chips with, so a root's pop/core wedge below\n // is guaranteed to fit within its actual sector without spilling into a neighbor's.\n const sectorSpanByRootId = useMemo(() => {\n const map = new Map<string, number>();\n if (groups.length <= 1) return map;\n const widths = computeSectorWidths(rootWeights);\n groups.forEach((group, index) => {\n map.set(group.root.id, widths[index]);\n });\n return map;\n }, [groups, rootWeights]);\n\n const wedgeSpanForRoot = useCallback(\n (rootId: string) => sectorSpanByRootId.get(rootId) ?? POP_WEDGE_SPAN_DEGREES,\n [sectorSpanByRootId],\n );\n\n // Each root's own single angle on the ring — unlike a branch's sub-wedge angle (which fans that\n // branch's subtree out at its own position within the root's wedge), a link from the root down\n // to any of its branches must originate from the root's own unique chip position.\n const rootAngleByRootId = useMemo(() => {\n const map = new Map<string, number>();\n groups.forEach((group, index) => {\n map.set(group.root.id, layout[index]?.angle ?? 0);\n });\n return map;\n }, [groups, layout]);\n\n // A root's wedge is subdivided across its own pop (resp. core) branches by each branch's\n // measured need — mirrors how ring roots themselves share the full circle.\n const placeBranches = useCallback(\n (side: \"pop\" | \"core\") => {\n const map = new Map<\n string,\n { hierarchy: d3.HierarchyNode<GenreTreeNode>; angle: number; span: number }[]\n >();\n groups.forEach((group) => {\n const hierarchies = branchHierarchiesByRootId.get(group.root.id)![side];\n if (hierarchies.length === 0) return;\n const slices = subdivideWedge(\n rootAngleByRootId.get(group.root.id)!,\n wedgeSpanForRoot(group.root.id),\n branchNeedsByRootId.get(group.root.id)![side],\n );\n map.set(\n group.root.id,\n hierarchies.map((hierarchy, branchIndex) => ({\n hierarchy,\n angle: slices[branchIndex]!.centerDeg,\n span: slices[branchIndex]!.spanDeg,\n })),\n );\n });\n return map;\n },\n [groups, branchHierarchiesByRootId, branchNeedsByRootId, rootAngleByRootId, wedgeSpanForRoot],\n );\n const popHierarchyByRootId = useMemo(() => placeBranches(\"pop\"), [placeBranches]);\n const coreHierarchyByRootId = useMemo(() => placeBranches(\"core\"), [placeBranches]);\n\n // The SVG canvas's actual radius. Core branches render outward from the real coreRootCircleRadius\n // (see computeCoreRadialLayout's call below), so the canvas must extend maxCoreExtentDelta past\n // that actual base — not past the ring-radius floor, which can be smaller than coreRootCircleRadius\n // whenever pop reach or the center subtree is what's sizing it — or the deepest core\n // node gets clipped. Purely a rendering-surface concern: never feeds back into coreRootCircleRadius,\n // so it doesn't affect the visual outer circle or ring root chip placement.\n const svgCanvasRadius = useMemo(\n () => coreRootCircleRadius + maxCoreExtentDelta,\n [coreRootCircleRadius, maxCoreExtentDelta],\n );\n\n // Boundary the center Mainstream Pop node's subtree currently occupies, drawn as a cosmetic\n // marker — the subtree itself renders inside this circle (see computeCenterRadialLayout below),\n // while every root's pop wedges fan outward from it toward coreRootCircleRadius.\n const middleCircleFloor = mainstreamCircleRadius;\n\n // Read via a ref rather than depending on `onRootSelect` directly — consumers commonly pass an\n // inline callback, which would otherwise re-fire this effect (and any state it sets) every render.\n const onRootSelectRef = useRef(onRootSelect);\n useEffect(() => {\n onRootSelectRef.current = onRootSelect;\n });\n\n useEffect(() => {\n if (effectiveTopRootId) onRootSelectRef.current?.(effectiveTopRootId);\n }, [effectiveTopRootId]);\n\n useEffect(() => {\n if (!popSvgRef.current) return;\n\n const svg = d3.select(popSvgRef.current);\n svg.selectAll(\"*\").remove();\n const originGroup = svg\n .append(\"g\")\n .attr(\"transform\", `translate(${svgCanvasRadius}, ${svgCanvasRadius})`);\n\n popHierarchyByRootId.forEach((branches, rootId) => {\n const rootLinkOrigin = getRadialPointOnCircle(\n rootAngleByRootId.get(rootId) ?? 0,\n coreRootCircleRadius,\n );\n\n const sectorGroup = originGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-pop-sector\")\n .attr(\"data-gtv-root-id\", rootId);\n\n branches.forEach(({ hierarchy, angle, span }, branchIndex) => {\n const laidOut = computePopRadialLayout(\n d3,\n hierarchy,\n angle,\n coreRootCircleRadius,\n span,\n POP_TREE_DEPTH_RADIAL_SPACING,\n );\n const reparentForbiddenIds = reparentingNodeId\n ? (laidOut\n .descendants()\n .find((d) => d.data.id === reparentingNodeId)\n ?.descendants()\n .map((d) => d.data.id) ?? [])\n : [];\n\n const branchGroup = sectorGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-pop-branch\")\n .attr(\"data-gtv-branch-index\", branchIndex);\n\n renderPopSubtree(\n d3,\n branchGroup,\n laidOut,\n getGenreNodeColor(index, rootId),\n reparentingNodeId,\n reparentForbiddenIds,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId)\n void onReparent?.(reparentingNodeId, newParentId);\n },\n onNodeClick: (data, event, clickedElement) => {\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n onNodeDoubleClick: (data) => focusOnNodeRef.current(data.id),\n onNodeHover: (data) => onNodeHoverRef.current?.(data),\n additionalActions,\n playingNodeId,\n playState,\n },\n wheelItemCountRange,\n {\n rootLinkOrigin,\n selectedNodeId: panel?.node.id ?? null,\n dotRadiusMemory: dotRadiusMemoryRef.current,\n nodeFillOf: (node) => getGenreNodeStyle(index, node.id).fill,\n },\n );\n });\n });\n\n coreHierarchyByRootId.forEach((branches, rootId) => {\n const rootLinkOrigin = getRadialPointOnCircle(\n rootAngleByRootId.get(rootId) ?? 0,\n coreRootCircleRadius,\n );\n\n const sectorGroup = originGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-core-sector\")\n .attr(\"data-gtv-root-id\", rootId);\n\n branches.forEach(({ hierarchy, angle, span }, branchIndex) => {\n const undergroundIds = new Set(\n hierarchy\n .descendants()\n .filter((d) => d.ancestors().some((a) => a.data.scene === \"underground\"))\n .map((d) => d.data.id),\n );\n const laidOut = computeCoreRadialLayout(\n d3,\n hierarchy,\n angle,\n span,\n coreRootCircleRadius,\n POP_TREE_DEPTH_RADIAL_SPACING,\n );\n const reparentForbiddenIds = reparentingNodeId\n ? (laidOut\n .descendants()\n .find((d) => d.data.id === reparentingNodeId)\n ?.descendants()\n .map((d) => d.data.id) ?? [])\n : [];\n\n const angleOf = (d: d3.HierarchyNode<GenreTreeNode>) => {\n const deg = (Math.atan2(d.x!, -d.y!) * 180) / Math.PI;\n return angle + ((((deg - angle + 540) % 360) + 360) % 360) - 180;\n };\n const subtreeRange = (d: d3.HierarchyNode<GenreTreeNode>) => {\n const angles = d.descendants().map(angleOf);\n return { min: Math.min(...angles), max: Math.max(...angles) };\n };\n laidOut\n .descendants()\n .filter((d) => d.data.scene === \"underground\" && !d.ancestors().slice(1).some((a) => a.data.scene === \"underground\"))\n .forEach((ug) => {\n const mine = subtreeRange(ug);\n const siblings = ug.parent?.children ?? [];\n const at = siblings.indexOf(ug);\n const prev = at > 0 ? siblings[at - 1] : undefined;\n const next = at >= 0 ? siblings[at + 1] : undefined;\n const startDeg = prev ? (subtreeRange(prev).max + mine.min) / 2 : angle - span / 2;\n const endDeg = next ? (mine.max + subtreeRange(next).min) / 2 : angle + span / 2;\n const arcPath = d3.arc()({\n innerRadius: coreRootCircleRadius,\n outerRadius: SCENE_SECTOR_OUTER_RADIUS,\n startAngle: (startDeg * Math.PI) / 180,\n endAngle: (endDeg * Math.PI) / 180,\n });\n sectorGroup\n .append(\"path\")\n .attr(\"class\", \"gtv-wheel-scene-sector\")\n .attr(\"d\", arcPath)\n .attr(\"fill\", shadeSurface(getGenreNodeColor(index, rootId), UNDERGROUND_DARKEN_RATIO))\n .attr(\"fill-opacity\", UNDERGROUND_SECTOR_FILL_OPACITY);\n });\n\n const branchGroup = sectorGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-core-branch\")\n .attr(\"data-gtv-branch-index\", branchIndex);\n\n renderPopSubtree(\n d3,\n branchGroup,\n laidOut,\n getGenreNodeColor(index, rootId),\n reparentingNodeId,\n reparentForbiddenIds,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId)\n void onReparent?.(reparentingNodeId, newParentId);\n },\n onNodeClick: (data, event, clickedElement) => {\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n onNodeDoubleClick: (data) => focusOnNodeRef.current(data.id),\n onNodeHover: (data) => onNodeHoverRef.current?.(data),\n additionalActions,\n playingNodeId,\n playState,\n },\n wheelItemCountRange,\n {\n isCoreSector: true,\n rootLinkOrigin,\n selectedNodeId: panel?.node.id ?? null,\n dotRadiusMemory: dotRadiusMemoryRef.current,\n nodeFillOf: (node) => {\n const fill = getGenreNodeStyle(index, node.id).fill;\n return undergroundIds.has(node.id) ? shadeSurface(fill, UNDERGROUND_DARKEN_RATIO) : fill;\n },\n },\n );\n });\n });\n\n if (centerSubtreeHierarchy) {\n const laidOutCenter = computeCenterRadialLayout(\n d3,\n centerSubtreeHierarchy,\n 0,\n POP_TREE_DEPTH_RADIAL_SPACING,\n );\n const reparentForbiddenIds = reparentingNodeId\n ? (laidOutCenter\n .descendants()\n .find((d) => d.data.id === reparentingNodeId)\n ?.descendants()\n .map((d) => d.data.id) ?? [])\n : [];\n\n const centerSectorGroup = originGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-center-sector\");\n\n renderPopSubtree(\n d3,\n centerSectorGroup,\n laidOutCenter,\n centerNodeColor,\n reparentingNodeId,\n reparentForbiddenIds,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId)\n void onReparent?.(reparentingNodeId, newParentId);\n },\n onNodeClick: (data, event, clickedElement) => {\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n onNodeDoubleClick: (data) => focusOnNodeRef.current(data.id),\n onNodeHover: (data) => onNodeHoverRef.current?.(data),\n additionalActions,\n playingNodeId,\n playState,\n },\n wheelItemCountRange,\n {\n skipRootNode: true,\n isMainstreamSector: isCenterMainstream,\n selectedNodeId: panel?.node.id ?? null,\n dotRadiusMemory: dotRadiusMemoryRef.current,\n nodeFillOf: (node) => getGenreNodeStyle(index, node.id).fill,\n },\n );\n }\n\n // Each branch draws its own links, so a later branch's links would paint over earlier ones' labels.\n const linksLayer = originGroup.insert(\"g\", \":first-child\").attr(\"class\", \"gtv-wheel-links\").node()!;\n originGroup.selectAll<SVGPathElement, unknown>(\"path.gtv-link\").each(function () {\n linksLayer.appendChild(this);\n });\n\n renderRootLabels(originGroup, [\n {\n id: centerNode.id,\n name: centerNode.name,\n x: 0,\n y: 0,\n radius: centerChipDiameter / 2,\n itemCount: centerNode.itemCount,\n alwaysShown: true,\n },\n ...groups.map((group) => {\n const itemCount = rootItemCountById.get(group.root.id)!;\n const { x, y } = getRadialPointOnCircle(rootAngleByRootId.get(group.root.id) ?? 0, coreRootCircleRadius);\n return {\n id: group.root.id,\n name: group.root.name,\n x,\n y,\n radius: calculateNodeDotRadius(itemCount, wheelItemCountRange),\n itemCount,\n };\n }),\n ]);\n hideCollidingLabels(d3, originGroup, wheelItemCountRange, zoomScaleRef.current);\n // A root's name usually spills past its chip, so the label itself forwards clicks to the chip.\n originGroup\n .selectAll<SVGGElement, RootLabelDatum>(\"g.gtv-root-label\")\n .select(\"text\")\n .style(\"pointer-events\", \"all\")\n .style(\"cursor\", \"pointer\")\n .on(\"click\", (_event, d) => {\n const chip =\n d.id === centerNode.id\n ? centerChipRef.current\n : viewportRef.current?.querySelector<HTMLButtonElement>(`#group-${CSS.escape(d.id)}`);\n chip?.click();\n })\n .on(\"dblclick\", (_event, d) => focusOnNodeRef.current(d.id));\n }, [\n centerNode,\n centerHasPopDisc,\n centerChipDiameter,\n groups,\n rootItemCountById,\n index,\n popHierarchyByRootId,\n coreHierarchyByRootId,\n coreRootCircleRadius,\n svgCanvasRadius,\n mainstreamCircleRadius,\n rootAngleByRootId,\n reparentingNodeId,\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n centerSubtreeHierarchy,\n centerNodeColor,\n isCenterMainstream,\n wheelItemCountRange,\n panel?.node.id,\n ]);\n\n // Re-places dot labels once a zoom gesture settles, so the set of named nodes follows the zoom\n // like place names on a map — kept out of the render effect above, which must not re-mount on zoom.\n useEffect(() => {\n const timeout = setTimeout(() => {\n if (popSvgRef.current) hideCollidingLabels(d3, d3.select(popSvgRef.current), wheelItemCountRange, zoomScale);\n }, LABEL_RELAYOUT_DEBOUNCE_MS);\n return () => clearTimeout(timeout);\n }, [zoomScale, wheelItemCountRange]);\n\n // Google Maps-style region label: once pan/zoom settles with the viewport center inside a ring\n // root's sector whose own chip is off-screen, names that root in an empty spot. Kept in place\n // until it leaves the viewport, its root changes, or zoom drifts its on-screen size out of range.\n const { panX, panY } = panZoom;\n useEffect(() => {\n const timeout = setTimeout(() => {\n const viewport = viewportRef.current;\n const wheel = wheelCircleRef.current;\n if (!viewport || !wheel) return;\n const vp = viewport.getBoundingClientRect();\n const wheelRect = wheel.getBoundingClientRect();\n const wheelX = wheelRect.left + wheelRect.width / 2;\n const wheelY = wheelRect.top + wheelRect.height / 2;\n const sectors = groups.map((group) => ({\n id: group.root.id,\n center: continuousAngleByRootId.get(group.root.id) ?? 0,\n span: sectorSpanByRootId.get(group.root.id) ?? 360,\n }));\n const rootIdAt = (x: number, y: number) =>\n Math.hypot(x - wheelX, y - wheelY) <= mainstreamCircleRadius * zoomScale\n ? null\n : findSectorAtAngle((Math.atan2(x - wheelX, wheelY - y) * 180) / Math.PI, sectors);\n const isOnScreen = (r: DOMRect) =>\n r.right > vp.left && r.left < vp.right && r.bottom > vp.top && r.top < vp.bottom;\n\n const rootId = rootIdAt(vp.left + vp.width / 2, vp.top + vp.height / 2);\n const chip = rootId && viewport.querySelector(`#group-${CSS.escape(rootId)}`);\n if (!rootId || !chip || isOnScreen(chip.getBoundingClientRect())) {\n setRegionLabel(null);\n return;\n }\n\n const shown = regionLabelRef.current?.getBoundingClientRect();\n const shownPx = (regionLabel?.fontSize ?? 0) * zoomScale;\n if (\n regionLabel?.rootId === rootId &&\n shown &&\n shown.left >= vp.left &&\n shown.right <= vp.right &&\n shown.top >= vp.top &&\n shown.bottom <= vp.bottom &&\n shownPx >= REGION_LABEL_MIN_PX &&\n shownPx <= REGION_LABEL_MAX_PX\n )\n return;\n\n const name = index.byId.get(rootId)?.name ?? \"\";\n const obstacles = Array.from(\n viewport.querySelectorAll(\n \".gtv-node-dot, .gtv-node-label:not(.gtv-node-label--hidden), .gtv-root-label, .gtv-wheel-chip, .gtv-wheel-floating-controls, .gtv-info-panel\",\n ),\n )\n .map((element) => element.getBoundingClientRect())\n .filter((r) => r.width > 0 && r.height > 0)\n .map((r) => ({ x0: r.left, y0: r.top, x1: r.right, y1: r.bottom }));\n const inset = REGION_LABEL_TARGET_PX / 2;\n const spot = pickEmptyLabelSpot(\n { x0: vp.left + inset, y0: vp.top + inset, x1: vp.right - inset, y1: vp.bottom - inset },\n name.length * REGION_LABEL_TARGET_PX * LABEL_CHAR_WIDTH_RATIO,\n REGION_LABEL_TARGET_PX,\n obstacles,\n (x, y) => rootIdAt(x, y) === rootId,\n );\n setRegionLabel(\n spot && {\n rootId,\n x: (spot.x - vp.left - panX) / zoomScale,\n y: (spot.y - vp.top - panY) / zoomScale,\n fontSize: REGION_LABEL_TARGET_PX / zoomScale,\n },\n );\n }, LABEL_RELAYOUT_DEBOUNCE_MS);\n return () => clearTimeout(timeout);\n }, [\n panX,\n panY,\n zoomScale,\n groups,\n continuousAngleByRootId,\n sectorSpanByRootId,\n mainstreamCircleRadius,\n index,\n regionLabel,\n ]);\n\n // Starts the view fit to the wheel + rendered pop sectors instead of at scale 1 / pan (0, 0) —\n // guarded so it only fires once popSvgRef has actually rendered content, and never again\n // afterward so it doesn't fight the user's own pan/zoom on later selections/expansions.\n const hasInitialFitRef = useRef(false);\n useEffect(() => {\n hasInitialFitRef.current = false;\n }, [focusNodeId]);\n useEffect(() => {\n if (hasInitialFitRef.current) return;\n const elements = [\n wheelCircleRef.current,\n ...queryTreeContentElements(popSvgRef.current),\n ];\n if (!elements.some(Boolean)) return;\n hasInitialFitRef.current = true;\n panZoom.fitToFrame(elements, { silent: true });\n });\n\n const handleChipClick = (rootId: string) => {\n setTopRootId(rootId);\n if (allowWheelRotation) setRotationTopRootId(rootId);\n };\n\n // Mirrors the onNodeClick/onSelectNode handling below: once the node's sector is mounted, center the viewport on the\n // externally-selected node and open its info panel exactly as a direct click would.\n useEffect(() => {\n if (!selectedNodeId || openPanel?.node.id === selectedNodeId) return;\n const targetNode = index.byId.get(selectedNodeId);\n if (\n targetNode &&\n onFocusChange &&\n !centerSubtreeNodeIds.has(selectedNodeId) &&\n !ringRootById.has(selectedNodeId)\n ) {\n // Outside the current zoom (e.g. a search result): refocus on its top-level root, whose frame\n // holds its whole subtree (the whole tree for Mainstream Pop's lineage or a root itself);\n // this effect re-runs once the new frame is drawn.\n const [root] = index.ancestorsOf(selectedNodeId);\n onFocusChange(root && root.name !== CENTER_NODE_NAME && isZoomableNode(nodes, root.id) ? root.id : null, \"search\");\n return;\n }\n const element = viewportRef.current?.querySelector(\n `#group-${CSS.escape(selectedNodeId)}`,\n );\n if (!targetNode || !element) return;\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(targetNode, element, viewportRef.current, { notify: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps -- panZoom re-created on pan/zoom; guarded by the openPanel?.node.id check above\n }, [selectedNodeId, index, openPanel, showNodeInfo, centerSubtreeNodeIds, ringRootById]);\n\n // One divider per boundary between two angularly-adjacent ring roots — see WheelRadialCore's own\n // copy of this computation for why each root's own continuous angle plus half its\n // weight-proportional width (sectorSpanByRootId) lands exactly on the boundary with its next\n // neighbor.\n const dividerAngles = useMemo(() => {\n if (groups.length <= 1) return [];\n return groups.map((group) => {\n const angle = continuousAngleByRootId.get(group.root.id) ?? 0;\n const width = sectorSpanByRootId.get(group.root.id) ?? 0;\n return angle + width / 2;\n });\n }, [groups, continuousAngleByRootId, sectorSpanByRootId]);\n\n // One tinted sector fan per root, bounded by its own weight-proportional width — see\n // WheelRadialCore's own copy of this computation, and computeSectorWidths's doc comment.\n const sectorFills = useMemo(() => {\n if (groups.length <= 1) return [];\n return groups.map((group) => {\n const angle = continuousAngleByRootId.get(group.root.id) ?? 0;\n const width = sectorSpanByRootId.get(group.root.id) ?? 0;\n return {\n rootId: group.root.id,\n start: angle - width / 2,\n clipPath: buildSectorClipPathPolygon(width),\n color: hexToRgba(\n getGenreNodeColor(index, group.root.id),\n ROOT_SECTOR_FILL_OPACITY,\n ),\n };\n });\n }, [groups, continuousAngleByRootId, sectorSpanByRootId]);\n\n return (\n <div\n ref={viewportRef}\n aria-busy={busy}\n className={[\n \"gtv-wheel-container\",\n \"gtv-wheel-container--radial\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n \"--gtv-wheel-radius\": `${coreRootCircleRadius}px`,\n \"--gtv-wheel-svg-radius\": `${svgCanvasRadius}px`,\n \"--gtv-wheel-rotation-transition-ms\": `${WHEEL_ROTATION_TRANSITION_MS}ms`,\n \"--gtv-wheel-rotation-easing\": WHEEL_ROTATION_EASING,\n touchAction: panZoom.touchAction,\n } as React.CSSProperties\n }\n onPointerDown={panZoom.handlePointerDown}\n >\n <div\n ref={stageRef}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n transform: panZoom.transform,\n transformOrigin: \"0 0\",\n }}\n >\n <div className=\"gtv-wheel-stage\">\n <div className=\"gtv-wheel-circle\" ref={wheelCircleRef} />\n\n {isPopExpanded && (\n <div\n className=\"gtv-wheel-middle-circle\"\n style={\n {\n \"--gtv-wheel-middle-radius\": `${middleCircleFloor}px`,\n } as React.CSSProperties\n }\n />\n )}\n\n {/* Sector wash + inner tint sit in their own .gtv-wheel layer, rendered before the node\n svg below, so they paint under the developed nodes instead of dulling them — see the\n second .gtv-wheel layer (after the svg) for the dividers/chips, which stay above the\n nodes. */}\n <div className=\"gtv-wheel\">\n {sectorFills.map((sector) => (\n <div\n key={`sector-${sector.rootId}`}\n className=\"gtv-wheel-sector\"\n style={\n {\n \"--gtv-sector-angle\": `${sector.start}deg`,\n \"--gtv-sector-color\": sector.color,\n clipPath: sector.clipPath,\n } as React.CSSProperties\n }\n />\n ))}\n\n <div className=\"gtv-wheel-inner-tint\" />\n\n {isPopExpanded && (\n <div\n className=\"gtv-wheel-middle-tint\"\n style={\n {\n \"--gtv-wheel-middle-radius\": `${middleCircleFloor}px`,\n } as React.CSSProperties\n }\n />\n )}\n </div>\n\n <svg\n ref={popSvgRef}\n className=\"gtv-wheel-pop-layer\"\n width={svgCanvasRadius * 2}\n height={svgCanvasRadius * 2}\n />\n\n <div className=\"gtv-wheel-center-node\">\n <div className=\"gtv-wheel-chip-anchor\">\n <button\n ref={centerChipRef}\n type=\"button\"\n className={[\n \"gtv-wheel-chip\",\n centerHasPopDisc && \"gtv-wheel-chip--center\",\n \"gtv-wheel-chip--circle\",\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n width: centerChipDiameter,\n \"--gtv-wheel-chip-base-height\": `${centerChipDiameter}px`,\n \"--gtv-chip-color\": centerNodeColor,\n ...(panel?.node.id === centerNode.id && {\n borderColor: ACCENT_COLOR,\n borderWidth: calculateSelectedNodeBorderWidth(centerChipDiameter / 2),\n }),\n } as React.CSSProperties\n }\n onClick={(event) => {\n const chip = event.currentTarget;\n const nativeEvent = event.nativeEvent;\n chipClickRef.current.schedule(() => {\n panZoom.centerOnElement(\n chip,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(chip, viewportRef.current, INFO_PANEL_WIDTH),\n );\n if (!reparentingNodeId) {\n showNodeInfo(centerNode, chip, viewportRef.current);\n onNodeClick?.(centerNode, nativeEvent);\n }\n });\n }}\n onDoubleClick={() => focusOnNode(centerNode.id)}\n aria-label={centerNode.name}\n />\n </div>\n </div>\n\n <div className=\"gtv-wheel\">\n {dividerAngles.map((angle, index) => (\n <div\n key={`divider-${groups[index].root.id}`}\n className=\"gtv-wheel-divider\"\n style={\n {\n \"--gtv-divider-angle\": `${angle}deg`,\n } as React.CSSProperties\n }\n />\n ))}\n\n {groups.map((group, groupIndex) => {\n const slot = layout[groupIndex];\n const root = index.byId.get(group.root.id)!;\n const angle =\n continuousAngleByRootId.get(group.root.id) ?? slot?.angle ?? 0;\n const chipColor = getGenreNodeStyle(index, group.root.id).fill;\n const rootItemCount = rootItemCountById.get(\n group.root.id,\n )!;\n const diameter = 2 * calculateNodeDotRadius(rootItemCount, wheelItemCountRange);\n return (\n <div\n key={group.root.id}\n className=\"gtv-wheel-slot\"\n style={\n { \"--gtv-chip-angle\": `${angle}deg` } as React.CSSProperties\n }\n >\n <div className=\"gtv-wheel-chip-anchor\">\n <button\n type=\"button\"\n id={`group-${group.root.id}`}\n className=\"gtv-wheel-chip gtv-wheel-chip--selected gtv-wheel-chip--circle\"\n style={\n {\n width: diameter,\n \"--gtv-wheel-chip-base-height\": `${diameter}px`,\n \"--gtv-chip-color\": chipColor,\n ...(panel?.node.id === group.root.id && {\n borderColor: ACCENT_COLOR,\n borderWidth: calculateSelectedNodeBorderWidth(diameter / 2),\n }),\n } as React.CSSProperties\n }\n aria-label={group.root.name}\n onPointerEnter={() => onNodeHover?.(root)}\n onClick={(event) => {\n const chip = event.currentTarget;\n const nativeEvent = event.nativeEvent;\n chipClickRef.current.schedule(() => {\n panZoom.centerOnElement(\n chip,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(chip, viewportRef.current, INFO_PANEL_WIDTH),\n );\n handleChipClick(group.root.id);\n if (!reparentingNodeId) {\n showNodeInfo(root, chip, viewportRef.current);\n onNodeClick?.(root, nativeEvent);\n }\n });\n }}\n onDoubleClick={() => focusOnNode(group.root.id)}\n />\n </div>\n </div>\n );\n })}\n </div>\n </div>\n {regionLabel && index.byId.has(regionLabel.rootId) && (\n <div\n ref={regionLabelRef}\n className=\"gtv-wheel-region-label\"\n aria-hidden=\"true\"\n style={\n {\n \"--gtv-region-label-x\": `${regionLabel.x}px`,\n \"--gtv-region-label-y\": `${regionLabel.y}px`,\n \"--gtv-region-label-size\": `${regionLabel.fontSize}px`,\n \"--gtv-region-label-color\": getGenreNodeColor(index, regionLabel.rootId),\n } as React.CSSProperties\n }\n >\n {index.byId.get(regionLabel.rootId)!.name}\n </div>\n )}\n </div>\n\n {panel && (\n <InfoPanel\n node={panel.node}\n {...getNodeVisualStyle(panel.node)}\n parentNode={(() => {\n const parent = index.byId.get(index.byId.get(panel.node.id)?.parentId ?? \"\");\n return parent\n ? { node: parent, ...getNodeVisualStyle(parent) }\n : null;\n })()}\n childNodes={(index.childrenByParentId.get(panel.node.id) ?? []).map(\n (n) => ({ node: n, ...getNodeVisualStyle(n) }),\n )}\n ancestorNodes={index.ancestorsOf(panel.node.id).map(\n (n): InfoPanelChild => ({ node: n, ...getNodeVisualStyle(n) }),\n )}\n side={panel.side}\n onClose={closeNodeInfo}\n onSelectNode={selectNodeFromPanel}\n onPrevious={\n typeof previousSelectedId === \"string\"\n ? () => {\n markPreviousRequested(previousSelectedId);\n selectNodeFromPanel(previousSelectedId);\n }\n : undefined\n }\n previousLabel={typeof previousSelectedId === \"string\" ? index.byId.get(previousSelectedId)?.name : undefined}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n }\n : undefined\n }\n renderInfoPanelBody={renderInfoPanelBody}\n onZoomIn={\n onFocusChange && panel.node.id !== focusNodeId && isZoomableNode(nodes, panel.node.id)\n ? () => onFocusChange(panel.node.id, \"zoom-in\")\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n\n {onFocusChange && focusNodeId !== null && (\n <WheelBreadcrumb\n key={focusNodeId}\n ancestors={index.ancestorsOf(focusNodeId).filter((ancestor) => isZoomableNode(nodes, ancestor.id))}\n currentName={index.byId.get(focusNodeId)?.name}\n onHome={() => {\n closeNodeInfo();\n onFocusChange(null, \"root\");\n }}\n onAncestor={(id) => {\n closeNodeInfo();\n onFocusChange(id, \"back\");\n }}\n />\n )}\n\n <FloatingControls\n panZoom={panZoom}\n orientation={controlsOrientation}\n leading={floatingControlsLeading}\n busy={busy}\n onFit={() => panZoom.fitToFrame([\n wheelCircleRef.current,\n ...queryTreeContentElements(popSvgRef.current),\n ])}\n />\n </div>\n );\n}\n","import { useState } from \"react\";\n\ninterface FocusHistoryState {\n seen: string | null;\n stack: (string | null)[];\n pending: string | null | undefined;\n}\n\n/** Remembers the focus the wheel actually displayed, whatever changed it (wheel buttons, search,\n * rejection, or the consumer). `previous` is the latest remembered view that is still focusable and\n * differs from the current one; `undefined` means there is nothing to go back to. */\nexport function useFocusHistory(focusNodeId: string | null, isFocusable: (id: string) => boolean) {\n const [state, setState] = useState<FocusHistoryState>({ seen: focusNodeId, stack: [], pending: undefined });\n\n if (state.seen !== focusNodeId) {\n // ponytail: unbounded, in-memory only; cap it if sessions ever get long enough to matter.\n const popTo = state.pending === focusNodeId ? state.stack.lastIndexOf(focusNodeId) : -1;\n setState({\n seen: focusNodeId,\n stack: popTo >= 0 ? state.stack.slice(0, popTo) : [...state.stack, state.seen],\n pending: undefined,\n });\n }\n\n let previous: string | null | undefined;\n for (let i = state.stack.length - 1; i >= 0; i--) {\n const entry = state.stack[i];\n if (entry !== focusNodeId && (entry === null || isFocusable(entry))) {\n previous = entry;\n break;\n }\n }\n\n const markPreviousRequested = (id: string | null) => setState((current) => ({ ...current, pending: id }));\n\n return { previous, markPreviousRequested };\n}\n","import { Fragment, useState } from \"react\";\nimport { MdHome } from \"react-icons/md\";\n\nimport type { GenreTreeNode } from \"./types\";\n\nconst MAX_VISIBLE_ANCESTORS = 3;\nconst ANCESTORS_KEPT_WHEN_COLLAPSED = 2;\n\ninterface WheelBreadcrumbProps {\n /** Zoomable ancestors of the focused view, root first. */\n ancestors: readonly GenreTreeNode[];\n currentName: string | undefined;\n onHome: () => void;\n onAncestor: (id: string) => void;\n}\n\n/** The focused wheel's trail: Home, its zoomable ancestor views, then the current view. A long trail\n * collapses its middle into a \"…\" button that expands it inline. */\nexport function WheelBreadcrumb({ ancestors, currentName, onHome, onAncestor }: WheelBreadcrumbProps) {\n const [expanded, setExpanded] = useState(false);\n const collapsed = !expanded && ancestors.length > MAX_VISIBLE_ANCESTORS;\n const visibleAncestors = collapsed ? ancestors.slice(-ANCESTORS_KEPT_WHEN_COLLAPSED) : ancestors;\n const separator = <span aria-hidden=\"true\">›</span>;\n\n return (\n <nav className=\"gtv-wheel-breadcrumb\" aria-label=\"Breadcrumb\">\n <button\n type=\"button\"\n className=\"gtv-wheel-breadcrumb-crumb gtv-wheel-breadcrumb-home\"\n aria-label=\"All genres\"\n title=\"All genres\"\n onClick={onHome}\n >\n <MdHome className=\"gtv-icon\" size={14} />\n </button>\n {separator}\n {collapsed && (\n <>\n <button\n type=\"button\"\n className=\"gtv-wheel-breadcrumb-crumb\"\n aria-label=\"Show full path\"\n aria-expanded={false}\n onClick={() => setExpanded(true)}\n >\n …\n </button>\n {separator}\n </>\n )}\n {visibleAncestors.map((ancestor) => (\n <Fragment key={ancestor.id}>\n <button type=\"button\" className=\"gtv-wheel-breadcrumb-crumb\" onClick={() => onAncestor(ancestor.id)}>\n {ancestor.name}\n </button>\n {separator}\n </Fragment>\n ))}\n <span className=\"gtv-wheel-breadcrumb-current\" aria-current=\"page\">\n {currentName}\n </span>\n </nav>\n );\n}\n","import { CENTER_NODE_NAME } from \"./constants\";\nimport { GenreTreeNode } from \"./types\";\n\nexport interface PopCoreFrame {\n /** The node rendered at the wheel's center. */\n center: GenreTreeNode;\n /** True when the center renders as the white pop disc with its own subtree inside; false when\n * it's shown as a label only (its children are the ring roots instead). */\n centerHasPopDisc: boolean;\n /** The center plus, when it has a pop disc, its whole subtree. */\n centerNodes: GenreTreeNode[];\n /** Every node rendered on the ring, each ring root's `parentId` rewritten to null (as is the\n * center's, so each subtree stratifies on its own). */\n ringNodes: GenreTreeNode[];\n}\n\nfunction collectSubtree(root: GenreTreeNode, childrenByParentId: Map<string, GenreTreeNode[]>) {\n const subtree: GenreTreeNode[] = [];\n const stack = [root];\n while (stack.length > 0) {\n const current = stack.pop()!;\n subtree.push(current);\n stack.push(...(childrenByParentId.get(current.id) ?? []));\n }\n return subtree;\n}\n\n/**\n * Whether `id` can be the pop-core wheel's focus: a known node with children and at most one pop\n * child (`resolvePopCoreFrame` can't center more than one pop disc).\n */\nexport function isZoomableNode(nodes: readonly GenreTreeNode[], id: string): boolean {\n if (!nodes.some((node) => node.id === id)) return false;\n const children = nodes.filter((node) => node.parentId === id);\n return children.length > 0 && children.filter((child) => child.side === \"pop\").length <= 1;\n}\n\n/**\n * Picks what the pop-core wheel renders. Without a focus, the \"Mainstream Pop\" root is the center\n * and every other root is a ring root. With a focus, the focus node's single pop-side child (if\n * any) becomes the center pop disc, otherwise the focus node itself is the center (label only);\n * every other direct child of the focus node becomes a ring root.\n */\nexport function resolvePopCoreFrame(nodes: GenreTreeNode[], focusNodeId: string | null): PopCoreFrame {\n const childrenByParentId = new Map<string, GenreTreeNode[]>();\n nodes.forEach((node) => {\n if (node.parentId === null) return;\n const siblings = childrenByParentId.get(node.parentId);\n if (siblings) siblings.push(node);\n else childrenByParentId.set(node.parentId, [node]);\n });\n\n if (focusNodeId === null) {\n const center = nodes.find((node) => node.parentId === null && node.name === CENTER_NODE_NAME);\n if (!center) {\n throw new Error(`GenreTreeWheelRadialPopCore requires a root node named \"${CENTER_NODE_NAME}\"`);\n }\n const centerNodes = collectSubtree(center, childrenByParentId);\n const centerIds = new Set(centerNodes.map((node) => node.id));\n return {\n center,\n centerHasPopDisc: true,\n centerNodes,\n ringNodes: nodes.filter((node) => !centerIds.has(node.id)),\n };\n }\n\n const focus = nodes.find((node) => node.id === focusNodeId);\n if (!focus) throw new Error(`Unknown pop-core focus node \"${focusNodeId}\"`);\n const children = childrenByParentId.get(focus.id) ?? [];\n const popChildren = children.filter((child) => child.side === \"pop\");\n if (popChildren.length > 1) {\n throw new Error(`Pop-core focus node \"${focus.id}\" has more than one pop child`);\n }\n const asRoot = (root: GenreTreeNode) =>\n collectSubtree(root, childrenByParentId).map((node) => (node === root ? { ...node, parentId: null } : node));\n const [popChild] = popChildren;\n const ringNodes = children.filter((child) => child !== popChild).flatMap(asRoot);\n if (!popChild) {\n const center = { ...focus, parentId: null };\n return { center, centerHasPopDisc: false, centerNodes: [center], ringNodes };\n }\n const centerNodes = asRoot(popChild);\n return { center: centerNodes[0], centerHasPopDisc: true, centerNodes, ringNodes };\n}\n","\"use client\";\n\nimport { WheelRadialPopCoreCore, WheelRadialPopCoreProps } from \"./GenreTreeWheelRadialPopCoreBase\";\n\nexport type GenreTreeWheelRadialPopCoreProps = WheelRadialPopCoreProps;\n\n/**\n * `GenreTreeWheelRadial`, but each developed root's outward subtree is only its core (non-pop)\n * branch — if that root also has a `side: \"pop\"` child, that pop subtree renders as a full\n * interactive tree fanned out inside the wheel's own circle instead. The wheel's pivot point\n * renders `nodes`' root named \"Pop\" (which must exist and have no children) as a full interactive\n * chip, styled the same as the ring's own root chips, instead of a plain label, and that root is\n * excluded from the ring's own chips.\n */\nexport function GenreTreeWheelRadialPopCore(props: GenreTreeWheelRadialPopCoreProps) {\n return <WheelRadialPopCoreCore {...props} />;\n}\n","\"use client\";\n\nimport {\n memo,\n MouseEvent as ReactMouseEvent,\n useLayoutEffect,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\n\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\nimport { PopCoreSplit, splitRootGroupBySide } from \"./pop-core-split\";\nimport { groupNodesByRoot } from \"./root-grouping\";\nimport { TreeIndex } from \"./tree-index\";\nimport { GenreTreeAction, GenreTreeNode, GenreTreeProps } from \"./types\";\nimport { useTreeIndex } from \"./use-tree-index\";\nimport {\n ACCENT_TEXT_COLOR,\n CENTER_NODE_NAME,\n getGenreTreeColor,\n POP_SECTOR_TINT_RATIO,\n TEXT_COLOR,\n TEXT_MUTED_COLOR,\n tintSurface,\n} from \"./constants\";\n\nexport type GenreTreeOutlineProps = Omit<\n GenreTreeProps,\n | \"rootColor\"\n | \"orientation\"\n | \"hideRoot\"\n | \"interactive\"\n | \"depthSpacingScale\"\n | \"wheelZoom\"\n | \"controlsOrientation\"\n | \"floatingControlsLeading\"\n | \"busy\"\n>;\n\ninterface OutlineState {\n selectedId: string | null;\n /** Node ids, plus `sectionKey`s, whose `<details>` is expanded. */\n openIds: ReadonlySet<string>;\n reparentingNodeId: string | null;\n reparentForbiddenIds: ReadonlySet<string>;\n}\n\ninterface OutlineStore {\n get: () => OutlineState;\n set: (patch: Partial<OutlineState>) => void;\n subscribe: (listener: () => void) => () => void;\n}\n\nfunction createOutlineStore(initial: OutlineState): OutlineStore {\n let state = initial;\n const listeners = new Set<() => void>();\n return {\n get: () => state,\n set: (patch) => {\n state = { ...state, ...patch };\n listeners.forEach((listener) => listener());\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n\n// Selectors return primitives, so a row only re-renders when its own slice changes.\nfunction useOutlineState<T>(store: OutlineStore, selector: (state: OutlineState) => T): T {\n return useSyncExternalStore(store.subscribe, () => selector(store.get()));\n}\n\ntype SectionKind = \"core\" | \"pop\";\n// NUL prefix keeps section keys from ever colliding with a node id.\nconst sectionKey = (kind: SectionKind, rootId: string) => `\\u0000${kind}:${rootId}`;\n\ninterface OutlineModel {\n index: TreeIndex;\n centerRoot: GenreTreeNode;\n ringRoots: GenreTreeNode[];\n detachedRoots: GenreTreeNode[];\n ringSplitById: ReadonlyMap<string, PopCoreSplit>;\n styleOf: (node: GenreTreeNode) => { fill: string; textColor: string };\n}\n\nfunction buildOutlineModel(nodes: readonly GenreTreeNode[], index: TreeIndex): OutlineModel {\n const groups = groupNodesByRoot([...nodes]);\n const centerGroup = groups.find((group) => group.root.name === CENTER_NODE_NAME);\n if (!centerGroup) {\n throw new Error(`GenreTreeOutline requires a root node named \"${CENTER_NODE_NAME}\"`);\n }\n const detachedGroups = groups.filter((group) => group.root.detached);\n const detachedRootIds = new Set(detachedGroups.map((group) => group.root.id));\n const ringGroups = groups.filter((group) => group !== centerGroup && !group.root.detached);\n const ringSplitById = new Map(ringGroups.map((group) => [group.root.id, splitRootGroupBySide(group)]));\n const popNodeIds = new Set([...ringSplitById.values()].flatMap((split) => split.popNodes.map((node) => node.id)));\n\n // Same fill/text rules as GenreTreeWheelRadialPopCore's own getNodeVisualStyle: center subtree\n // tinted with dark text, a ring root's core side solid, its pop side tinted.\n const styleOf = (node: GenreTreeNode) => {\n const rootId = index.rootIdById.get(node.id) ?? node.id;\n if (rootId === centerGroup.root.id) {\n return { fill: tintSurface(\"#ffffff\", POP_SECTOR_TINT_RATIO), textColor: TEXT_COLOR };\n }\n if (detachedRootIds.has(rootId)) return { fill: TEXT_MUTED_COLOR, textColor: TEXT_COLOR };\n const rootColor = getGenreTreeColor(index.byId.get(rootId)?.wikidataId);\n return {\n fill: popNodeIds.has(node.id) ? tintSurface(rootColor, POP_SECTOR_TINT_RATIO) : rootColor,\n textColor: ACCENT_TEXT_COLOR,\n };\n };\n\n return {\n index,\n centerRoot: centerGroup.root,\n ringRoots: ringGroups.map((group) => group.root),\n detachedRoots: detachedGroups.map((group) => group.root),\n ringSplitById,\n styleOf,\n };\n}\n\n/** Identity-stable for the component's lifetime; every handler reads the latest props. */\ninterface OutlineCallbacks {\n onNameClick: (event: ReactMouseEvent<HTMLButtonElement>, node: GenreTreeNode) => void;\n onHover: (node: GenreTreeNode) => void;\n toggleOpen: (key: string) => void;\n onPlayPause: (nodeId: string) => void;\n onAddChild: (parentId: string) => void;\n onRenameRequest: (node: GenreTreeNode) => void;\n onDeleteRequest: (node: GenreTreeNode) => void;\n onReparentRequest: (node: GenreTreeNode) => void;\n additionalActions: (node: GenreTreeNode) => GenreTreeAction[];\n}\n\ninterface OutlineItemProps {\n model: OutlineModel;\n store: OutlineStore;\n callbacks: OutlineCallbacks;\n}\n\nfunction handleSummaryClick(event: ReactMouseEvent, toggle: () => void) {\n // The name button and toolbar preventDefault their own clicks so they never toggle the section.\n if (event.defaultPrevented) return;\n event.preventDefault();\n toggle();\n}\n\n// Declared unwrapped and memoized separately: a named `memo(function OutlineNode …)` would bind\n// the recursive `<OutlineNode>` below to the unmemoized inner function.\nfunction OutlineNodeView({\n nodeId,\n isRoot,\n model,\n store,\n callbacks,\n}: OutlineItemProps & { nodeId: string; isRoot: boolean }) {\n const node = model.index.byId.get(nodeId)!;\n const isSelected = useOutlineState(store, (s) => s.selectedId === nodeId);\n const isOpen = useOutlineState(store, (s) => s.openIds.has(nodeId));\n const reparentStatus = useOutlineState(store, (s) =>\n s.reparentingNodeId === null ? null : s.reparentForbiddenIds.has(nodeId) ? \"forbidden\" : \"target\",\n );\n\n const { itemCount } = node;\n const children = model.index.childrenByParentId.get(nodeId) ?? [];\n const split = isRoot ? model.ringSplitById.get(nodeId) : undefined;\n\n const row = (\n <span className={[\"gtv-outline-row\", isSelected && \"gtv-outline-row--selected\"].filter(Boolean).join(\" \")}>\n <span className=\"gtv-outline-dot\" style={{ background: model.styleOf(node).fill }} />\n <button\n type=\"button\"\n className={[\n \"gtv-outline-name\",\n isRoot && \"gtv-outline-name--root\",\n node.umbrella && \"gtv-outline-name--umbrella\",\n reparentStatus === \"target\" && \"gtv-outline-name--reparent-target\",\n ]\n .filter(Boolean)\n .join(\" \")}\n disabled={reparentStatus === \"forbidden\"}\n onClick={(event) => callbacks.onNameClick(event, node)}\n onPointerEnter={() => callbacks.onHover(node)}\n onFocus={() => callbacks.onHover(node)}\n >\n {node.name}\n </button>\n <span className=\"gtv-outline-count\">{itemCount}</span>\n </span>\n );\n\n const itemProps = { model, store, callbacks };\n return (\n <li className=\"gtv-outline-item\" data-gtv-node-id={nodeId}>\n {children.length > 0 ? (\n <details open={isOpen}>\n <summary onClick={(event) => handleSummaryClick(event, () => callbacks.toggleOpen(nodeId))}>{row}</summary>\n {isOpen && (\n <ul className=\"gtv-outline-list\">\n {split ? (\n <>\n <OutlineSection rootId={nodeId} kind=\"core\" branches={split.coreBranches} {...itemProps} />\n <OutlineSection rootId={nodeId} kind=\"pop\" branches={split.popBranches} {...itemProps} />\n </>\n ) : (\n children.map((child) => <OutlineNode key={child.id} nodeId={child.id} isRoot={false} {...itemProps} />)\n )}\n </ul>\n )}\n </details>\n ) : (\n <div className=\"gtv-outline-leaf\">{row}</div>\n )}\n </li>\n );\n}\nconst OutlineNode = memo(OutlineNodeView);\n\nconst OutlineSection = memo(function OutlineSection({\n rootId,\n kind,\n branches,\n model,\n store,\n callbacks,\n}: OutlineItemProps & { rootId: string; kind: SectionKind; branches: PopCoreSplit[\"coreBranches\"] }) {\n const key = sectionKey(kind, rootId);\n const isOpen = useOutlineState(store, (s) => s.openIds.has(key));\n if (branches.length === 0) return null;\n return (\n <li className=\"gtv-outline-item\">\n <details open={isOpen}>\n <summary onClick={(event) => handleSummaryClick(event, () => callbacks.toggleOpen(key))}>\n <span className=\"gtv-outline-section-label\">{kind === \"core\" ? \"Core\" : \"Pop\"}</span>\n </summary>\n {isOpen && (\n <ul className=\"gtv-outline-list\">\n {branches.map(({ child }) => (\n <OutlineNode key={child.id} nodeId={child.id} isRoot={false} model={model} store={store} callbacks={callbacks} />\n ))}\n </ul>\n )}\n </details>\n </li>\n );\n});\n\n/**\n * Text counterpart of `GenreTreeWheelRadialPopCore`: the same forest as nested collapsible lists\n * (`<details>`), \"Mainstream Pop\" first, then every other root with its direct children split\n * into \"Core\" and \"Pop\" sections. Same toolbar, reparent and info-panel behavior as the graphical\n * renderers.\n *\n * Selection and open sections live in a per-instance external store that each memoized row\n * subscribes to by its own id, so selecting a node re-renders only the two affected rows and the\n * panel, and a collapsed section's rows aren't rendered at all.\n */\nexport function GenreTreeOutline(props: GenreTreeOutlineProps) {\n const {\n nodes,\n className,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n showToolbar = true,\n selectedNodeId,\n renderInfoPanelBody,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n } = props;\n const scrollRef = useRef<HTMLDivElement>(null);\n const index = useTreeIndex(nodes);\n const model = useMemo(() => buildOutlineModel(nodes, index), [nodes, index]);\n\n const reparentForbiddenIds = useMemo(() => {\n const ids = new Set<string>();\n const stack = reparentingNodeId ? [reparentingNodeId] : [];\n while (stack.length > 0) {\n const id = stack.pop()!;\n ids.add(id);\n stack.push(...(index.childrenByParentId.get(id) ?? []).map((child) => child.id));\n }\n return ids;\n }, [reparentingNodeId, index]);\n\n const [store] = useState(() =>\n createOutlineStore({\n selectedId: null,\n openIds: new Set(),\n reparentingNodeId,\n reparentForbiddenIds,\n }),\n );\n useLayoutEffect(() => {\n store.set({ reparentingNodeId, reparentForbiddenIds });\n }, [store, reparentingNodeId, reparentForbiddenIds]);\n\n const latestProps = useRef(props);\n useLayoutEffect(() => {\n latestProps.current = props;\n });\n\n const callbacks = useMemo<OutlineCallbacks>(\n () => ({\n onNameClick: (event, node) => {\n // Keeps a name click from also toggling the enclosing <summary>'s section.\n event.preventDefault();\n const { reparentingNodeId: reparenting, onReparent, onNodeClick } = latestProps.current;\n if (reparenting) {\n void onReparent?.(reparenting, node.id);\n return;\n }\n const changed = store.get().selectedId !== node.id;\n store.set({ selectedId: node.id });\n if (changed) latestProps.current.onSelectedNodeChange?.(node);\n onNodeClick?.(node, event.nativeEvent);\n },\n onHover: (node) => latestProps.current.onNodeHover?.(node),\n toggleOpen: (key) => {\n const openIds = new Set(store.get().openIds);\n if (!openIds.delete(key)) openIds.add(key);\n store.set({ openIds });\n },\n onPlayPause: (nodeId) => latestProps.current.onPlayPause?.(nodeId),\n onAddChild: (parentId) => latestProps.current.onAddChild?.(parentId),\n onRenameRequest: (node) => latestProps.current.onRenameRequest?.(node),\n onDeleteRequest: (node) => latestProps.current.onDeleteRequest?.(node),\n onReparentRequest: (node) => latestProps.current.onReparentRequest?.(node),\n additionalActions: (node) => latestProps.current.additionalActions?.(node) ?? [],\n }),\n [store],\n );\n\n const selectedId = useOutlineState(store, (s) => s.selectedId);\n const pendingScrollIdRef = useRef<string | null>(null);\n\n // Expands every collapsed ancestor section of `nodeId`'s row, opens its info panel, and scrolls\n // it into view once rendered — shared by panel chip navigation and the externally-controlled\n // `selectedNodeId` (which passes `notify: false`, so it never echoes back). No-op for an id\n // that isn't in the tree.\n const selectNode = (nodeId: string, { notify = true }: { notify?: boolean } = {}) => {\n const ancestors = index.ancestorsOf(nodeId);\n if (!index.rootIdById.get(nodeId)) return;\n const openIds = new Set(store.get().openIds);\n for (const ancestor of ancestors) openIds.add(ancestor.id);\n const [root, branchChild = index.byId.get(nodeId)!] = ancestors;\n if (root && root.id !== model.centerRoot.id && !root.detached) {\n openIds.add(sectionKey(branchChild.side === \"pop\" ? \"pop\" : \"core\", root.id));\n }\n const changed = store.get().selectedId !== nodeId;\n if (changed) pendingScrollIdRef.current = nodeId;\n store.set({ openIds, selectedId: nodeId });\n if (changed && notify) latestProps.current.onSelectedNodeChange?.(index.byId.get(nodeId)!);\n };\n const selectNodeRef = useRef(selectNode);\n useLayoutEffect(() => {\n selectNodeRef.current = selectNode;\n });\n\n useLayoutEffect(() => {\n if (!selectedId || pendingScrollIdRef.current !== selectedId) return;\n pendingScrollIdRef.current = null;\n scrollRef.current\n ?.querySelector(`[data-gtv-node-id=\"${CSS.escape(selectedId)}\"]`)\n ?.scrollIntoView({ block: \"nearest\" });\n }, [selectedId]);\n\n useEffect(() => {\n if (!selectedNodeId || selectedId === selectedNodeId) return;\n selectNodeRef.current(selectedNodeId, { notify: false });\n }, [selectedNodeId, selectedId]);\n\n const prevSelectedNodeIdRef = useRef(selectedNodeId);\n useEffect(() => {\n if (prevSelectedNodeIdRef.current != null && selectedNodeId == null) store.set({ selectedId: null });\n prevSelectedNodeIdRef.current = selectedNodeId;\n }, [selectedNodeId, store]);\n\n const selectedNode = selectedId === null ? undefined : index.byId.get(selectedId);\n const itemProps = { model, store, callbacks };\n\n return (\n <div className={[\"gtv-outline\", selectedNode && \"gtv-outline--panel-open\", className].filter(Boolean).join(\" \")}>\n <div className=\"gtv-outline-scroll\" ref={scrollRef}>\n <ul className=\"gtv-outline-list gtv-outline-list--roots\">\n {[model.centerRoot, ...model.ringRoots].map((root) => (\n <OutlineNode key={root.id} nodeId={root.id} isRoot {...itemProps} />\n ))}\n </ul>\n {model.detachedRoots.length > 0 && (\n <ul className=\"gtv-outline-list gtv-outline-list--roots gtv-outline-list--detached\">\n {model.detachedRoots.map((root) => (\n <OutlineNode key={root.id} nodeId={root.id} isRoot {...itemProps} />\n ))}\n </ul>\n )}\n </div>\n\n {selectedNode && (\n <InfoPanel\n node={selectedNode}\n {...model.styleOf(selectedNode)}\n parentNode={(() => {\n const parent = index.byId.get(selectedNode.parentId ?? \"\");\n return parent ? { node: parent, ...model.styleOf(parent) } : null;\n })()}\n childNodes={(index.childrenByParentId.get(selectedNode.id) ?? []).map((n) => ({ node: n, ...model.styleOf(n) }))}\n ancestorNodes={index\n .ancestorsOf(selectedNode.id)\n .map((n): InfoPanelChild => ({ node: n, ...model.styleOf(n) }))}\n side=\"left\"\n onClose={() => {\n store.set({ selectedId: null });\n latestProps.current.onSelectedNodeChange?.(null);\n }}\n onSelectNode={(id) => selectNodeRef.current(id)}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n renderInfoPanelBody={renderInfoPanelBody}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause: callbacks.onPlayPause,\n onAddChild: callbacks.onAddChild,\n onRenameRequest: callbacks.onRenameRequest,\n onDeleteRequest: callbacks.onDeleteRequest,\n onReparentRequest: callbacks.onReparentRequest,\n additionalActions: callbacks.additionalActions,\n }\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n </div>\n );\n}\n","\"use client\";\n\nimport { useId } from \"react\";\n\n// These values approximate the real GenreTree's visual tokens (card corner radius, surface\n// and connector colors) rather than importing them, since the @behindthemusictree/genre-tree-view\n// package only publicly exports GenreTree, getGenreTreeColor, and types — not its internal\n// dimension/style constants.\nconst CARD_FILL = \"#F4F4F5\";\nconst CARD_BORDER_COLOR = \"#E4E4E7\";\nconst CONNECTOR_COLOR = \"#D4D4D8\";\nconst CORNER_RADIUS = 8;\nconst SHIMMER_HIGHLIGHT_COLOR = \"#FFFFFF\";\n\n// A real genre tree can run several levels deep and fan out into dozens of subgenres, so the\n// skeleton mirrors that with 6 columns (root through 5 descendant levels) and a tapering node\n// count per level, rather than a token 1-3-2 shape, to read as \"a big tree is loading\" instead\n// of \"a few items are loading\".\nconst LEVEL_COUNTS = [1, 6, 13, 15, 20, 25];\nconst LEVEL_WIDTHS = [200, 180, 165, 150, 140, 130];\nconst LEVEL_HEIGHTS = [36, 26, 20, 17, 15, 14];\n\nconst LEAF_ROW_HEIGHT = 20;\nconst GROUP_GAP = 12;\nconst COLUMN_GAP = 120;\nconst PADDING_X = 16;\nconst PADDING_Y = 20;\n\nconst SHIMMER_BAND_WIDTH = 160;\n\ntype Card = { x: number; y: number; width: number; height: number };\ntype Range = { start: number; end: number };\n\nfunction cardCenterY(card: Card) {\n return card.y + card.height / 2;\n}\n\n// Spreads `childCount` nodes as evenly as possible across `parentCount` parents (remainder\n// distributed to the first parents), so level sizes that don't divide evenly (e.g. 13 into 6)\n// still produce a contiguous, evenly-ordered range of children per parent.\nfunction distributeChildren(parentCount: number, childCount: number): Range[] {\n const base = Math.floor(childCount / parentCount);\n const remainder = childCount % parentCount;\n const ranges: Range[] = [];\n let cursor = 0;\n for (let p = 0; p < parentCount; p++) {\n const count = base + (p < remainder ? 1 : 0);\n ranges.push({ start: cursor, end: cursor + count - 1 });\n cursor += count;\n }\n return ranges;\n}\n\n// CHILD_RANGES[i][n] gives the range of level (i+1) node indices that are children of the n-th\n// node at level i.\nconst CHILD_RANGES: Range[][] = [];\nfor (let i = 0; i < LEVEL_COUNTS.length - 1; i++) {\n CHILD_RANGES.push(distributeChildren(LEVEL_COUNTS[i], LEVEL_COUNTS[i + 1]));\n}\n\nconst LEVEL_X: number[] = [];\n{\n let x = PADDING_X;\n for (let i = 0; i < LEVEL_COUNTS.length; i++) {\n LEVEL_X.push(x);\n x += LEVEL_WIDTHS[i] + COLUMN_GAP;\n }\n}\n\nconst LAST_LEVEL = LEVEL_COUNTS.length - 1;\nconst LEVEL_CARDS: Card[][] = new Array(LEVEL_COUNTS.length);\nlet VIEWBOX_HEIGHT: number;\n\n// Leaf level is laid out first (top to bottom, with an extra gap between sibling groups), then\n// every level above it is positioned from the vertical midpoint of its children, walking up to\n// the root.\n{\n const leafCards: Card[] = [];\n let cursorY = PADDING_Y;\n for (const range of CHILD_RANGES[LAST_LEVEL - 1]) {\n for (let i = range.start; i <= range.end; i++) {\n leafCards.push({\n x: LEVEL_X[LAST_LEVEL],\n y: cursorY,\n width: LEVEL_WIDTHS[LAST_LEVEL],\n height: LEVEL_HEIGHTS[LAST_LEVEL],\n });\n cursorY += LEAF_ROW_HEIGHT;\n }\n cursorY += GROUP_GAP;\n }\n LEVEL_CARDS[LAST_LEVEL] = leafCards;\n VIEWBOX_HEIGHT = cursorY - GROUP_GAP + PADDING_Y;\n}\n\nfor (let i = LAST_LEVEL - 1; i >= 0; i--) {\n const childCards = LEVEL_CARDS[i + 1];\n LEVEL_CARDS[i] = CHILD_RANGES[i].map(({ start, end }) => {\n const centerY =\n (cardCenterY(childCards[start]) + cardCenterY(childCards[end])) / 2;\n return {\n x: LEVEL_X[i],\n y: centerY - LEVEL_HEIGHTS[i] / 2,\n width: LEVEL_WIDTHS[i],\n height: LEVEL_HEIGHTS[i],\n };\n });\n}\n\nconst ALL_CARDS: { card: Card; rootAccent?: boolean }[] = LEVEL_CARDS.flatMap(\n (cards, level) =>\n cards.map((card) => ({ card, rootAccent: level === 0 })),\n);\n\nconst ALL_LINKS: { from: Card; to: Card }[] = [];\nfor (let i = 0; i < LAST_LEVEL; i++) {\n const parentCards = LEVEL_CARDS[i];\n const childCards = LEVEL_CARDS[i + 1];\n CHILD_RANGES[i].forEach(({ start, end }, parentIndex) => {\n for (let c = start; c <= end; c++) {\n ALL_LINKS.push({ from: parentCards[parentIndex], to: childCards[c] });\n }\n });\n}\n\nconst VIEWBOX_WIDTH = LEVEL_X[LAST_LEVEL] + LEVEL_WIDTHS[LAST_LEVEL] + PADDING_X;\n\nfunction cardCenterLeft(card: Card) {\n return { x: card.x, y: card.y + card.height / 2 };\n}\n\nfunction cardCenterRight(card: Card) {\n return { x: card.x + card.width, y: card.y + card.height / 2 };\n}\n\nfunction connectorPath(from: Card, to: Card) {\n const start = cardCenterRight(from);\n const end = cardCenterLeft(to);\n const midX = (start.x + end.x) / 2;\n return `M${start.x},${start.y} C${midX},${start.y} ${midX},${end.y} ${end.x},${end.y}`;\n}\n\nfunction SkeletonCard({\n card,\n rootAccent,\n}: {\n card: Card;\n rootAccent?: boolean;\n}) {\n return (\n <>\n <rect\n x={card.x}\n y={card.y}\n width={card.width}\n height={card.height}\n rx={CORNER_RADIUS}\n ry={CORNER_RADIUS}\n fill={CARD_FILL}\n stroke={CARD_BORDER_COLOR}\n strokeWidth={rootAccent ? 1.5 : 1}\n />\n {rootAccent && (\n <circle\n cx={card.x + 18}\n cy={card.y + card.height / 2}\n r={5}\n fill={CONNECTOR_COLOR}\n />\n )}\n </>\n );\n}\n\nexport function GenreTreeSkeleton() {\n // Unique per mount so multiple skeletons on one page don't collide on <defs> ids.\n const uid = useId().replace(/[^a-zA-Z0-9]/g, \"\");\n const gradientId = `genre-tree-skeleton-gradient-${uid}`;\n const maskId = `genre-tree-skeleton-mask-${uid}`;\n const animationName = `genre-tree-skeleton-sweep-${uid}`;\n const shimmerBandClass = `genre-tree-skeleton-shimmer-band-${uid}`;\n const sweepDistance = VIEWBOX_WIDTH + SHIMMER_BAND_WIDTH;\n\n return (\n <div className=\"gtv-skeleton\">\n <span className=\"gtv-skeleton-sr-only\">Loading genre tree…</span>\n <svg\n viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}\n width={VIEWBOX_WIDTH}\n height={VIEWBOX_HEIGHT}\n className=\"gtv-skeleton-svg\"\n aria-hidden=\"true\"\n >\n <style>{`\n @keyframes ${animationName} {\n from { transform: translateX(0); }\n to { transform: translateX(${sweepDistance}px); }\n }\n .${shimmerBandClass} {\n animation: ${animationName} 1.5s linear infinite;\n }\n @media (prefers-reduced-motion: reduce) {\n .${shimmerBandClass} {\n animation: none;\n }\n }\n `}</style>\n <defs>\n <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n <stop\n offset=\"0%\"\n stopColor={SHIMMER_HIGHLIGHT_COLOR}\n stopOpacity={0}\n />\n <stop\n offset=\"50%\"\n stopColor={SHIMMER_HIGHLIGHT_COLOR}\n stopOpacity={0.9}\n />\n <stop\n offset=\"100%\"\n stopColor={SHIMMER_HIGHLIGHT_COLOR}\n stopOpacity={0}\n />\n </linearGradient>\n {/* White = visible: masks the shimmer band to the tree's own silhouette so the sweep\n only lights up cards and connectors, never the empty space between them. */}\n <mask id={maskId}>\n {ALL_LINKS.map((link, i) => (\n <path\n key={`link-mask-${i}`}\n d={connectorPath(link.from, link.to)}\n fill=\"none\"\n stroke=\"#FFFFFF\"\n strokeWidth={1.25}\n />\n ))}\n {ALL_CARDS.map(({ card }, i) => (\n <rect\n key={`card-mask-${i}`}\n x={card.x}\n y={card.y}\n width={card.width}\n height={card.height}\n rx={CORNER_RADIUS}\n ry={CORNER_RADIUS}\n fill=\"#FFFFFF\"\n />\n ))}\n </mask>\n </defs>\n\n {ALL_LINKS.map((link, i) => (\n <path\n key={`link-${i}`}\n d={connectorPath(link.from, link.to)}\n fill=\"none\"\n stroke={CONNECTOR_COLOR}\n strokeWidth={1.25}\n />\n ))}\n {ALL_CARDS.map(({ card, rootAccent }, i) => (\n <SkeletonCard key={`card-${i}`} card={card} rootAccent={rootAccent} />\n ))}\n\n <g mask={`url(#${maskId})`}>\n <rect\n className={shimmerBandClass}\n x={-SHIMMER_BAND_WIDTH}\n y={0}\n width={SHIMMER_BAND_WIDTH}\n height={VIEWBOX_HEIGHT}\n fill={`url(#${gradientId})`}\n />\n </g>\n </svg>\n </div>\n );\n}\n","\"use client\";\n\nimport { useId, useLayoutEffect, useRef } from \"react\";\nimport { NODE_DOT_MIN_RADIUS, TREE_COLORS } from \"./constants\";\n\n// Neutral grays rather than the real wheel's per-root tints, so the loading state reads as a\n// placeholder — see GenreTreeSkeleton's comment on why these are approximated, not imported.\nconst SHAPE_COLOR = \"#D4D4D8\";\nconst SECTOR_FILL_OPACITY = 0.22;\nconst INNER_TINT_OPACITY = 0.55;\nconst POP_LINK_OPACITY = 0.6;\nconst CANVAS_PADDING = 20;\n\nconst N_ROOTS = 9;\n\n// Radii (from the wheel's center) mirror the real pop/core wheel: a small mainstream subtree at\n// the center, the ring roots on the circle bounding the white \"pop\" region, each root's pop\n// branch reaching inward and its core branch outward.\nconst MAINSTREAM_RADIUS = 70;\nconst RING_RADIUS = 300;\nconst POP_RADII = [245, 195, 145];\nconst CORE_RADII = [375, 445, 515, 575];\n\nconst ROOT_DOT_RADIUS_RANGE = 8;\nconst LEVEL1_DOT_RADIUS_RANGE = 3;\nconst LINK_WIDTH = 1;\nconst LEAF_LINK_WIDTH = 0.75;\n\n// Angles use the real wheel's convention (0 = top, clockwise), so the sector gradient below\n// lines up with the dots without any translation.\ntype Point = { x: number; y: number };\ntype Polar = { angle: number; radius: number; point: Point };\ntype Dot = { point: Point; r: number };\ntype Link = { d: string; width: number; opacity: number };\n\n// Math.sin/Math.cos aren't specified to the ULP by ECMAScript, so raw trig output can differ in\n// the last float digit between server (Node) and client (browser) V8. Rounding well below that\n// noise floor makes every coordinate byte-identical across environments, so this can safely SSR.\nconst COORDINATE_PRECISION = 1000;\n\nfunction round(value: number): number {\n return Math.round(value * COORDINATE_PRECISION) / COORDINATE_PRECISION;\n}\n\nfunction pointOnCircle(angleDeg: number, radius: number): Point {\n const rad = (angleDeg * Math.PI) / 180;\n return { x: round(radius * Math.sin(rad)), y: round(-radius * Math.cos(rad)) };\n}\n\n// Deterministic (not Math.random()) so server and client renders stay identical.\nfunction unitRandom(seed: number): number {\n const x = Math.sin(seed * 12.9898) * 43758.5453;\n return round(x - Math.floor(x));\n}\n\nfunction randomCount(seed: number, min: number, max: number): number {\n return Math.min(max, min + Math.floor(unitRandom(seed) * (max - min + 1)));\n}\n\nfunction fanAngles(center: number, spread: number, count: number): number[] {\n if (count <= 1) return [center];\n const step = spread / (count - 1);\n return Array.from({ length: count }, (_, i) => center - spread / 2 + i * step);\n}\n\n// Same shape as d3.linkRadial: a cubic whose control points sit at the mid radius on each end's\n// own angle, so branches curve along the wheel's rings instead of cutting chords across them.\nfunction linkPath(from: Polar, to: Polar): string {\n const midRadius = (from.radius + to.radius) / 2;\n const c0 = pointOnCircle(from.angle, midRadius);\n const c1 = pointOnCircle(to.angle, midRadius);\n return `M${from.point.x},${from.point.y} C${c0.x},${c0.y} ${c1.x},${c1.y} ${to.point.x},${to.point.y}`;\n}\n\nconst DOTS: Dot[] = [];\nconst LINKS: Link[] = [];\n\nfunction addNode(angle: number, radius: number, dotRadius: number): Polar {\n const point = pointOnCircle(angle, radius);\n DOTS.push({ point, r: dotRadius });\n return { angle, radius, point };\n}\n\nfunction addLink(from: Polar, to: Polar, width: number, opacity = 1) {\n LINKS.push({ d: linkPath(from, to), width, opacity });\n}\n\n// Roots share the circle unevenly, like the real wheel's weight-proportional sectors; each root\n// sits at the middle of its own span and its branches fan within it.\nconst ROOT_WEIGHTS = Array.from({ length: N_ROOTS }, (_, i) => 0.25 + 0.75 * unitRandom(i * 17 + 3));\nconst TOTAL_WEIGHT = ROOT_WEIGHTS.reduce((sum, weight) => sum + weight, 0);\n\ntype Sector = { startAngle: number; endAngle: number; color: string };\nconst SECTORS: Sector[] = [];\n{\n let cursor = (-360 * ROOT_WEIGHTS[0]) / TOTAL_WEIGHT / 2;\n ROOT_WEIGHTS.forEach((weight, i) => {\n const span = (360 * weight) / TOTAL_WEIGHT;\n SECTORS.push({ startAngle: cursor, endAngle: cursor + span, color: TREE_COLORS[i % TREE_COLORS.length] });\n cursor += span;\n });\n}\n\nfunction buildMainstream() {\n const center = addNode(0, 0, NODE_DOT_MIN_RADIUS * 2);\n const level1Count = 6;\n fanAngles(0, 360 - 360 / level1Count, level1Count).forEach((angle, i) => {\n const level1 = addNode(angle, 26, NODE_DOT_MIN_RADIUS * 0.8);\n addLink(center, level1, LINK_WIDTH);\n const leafCount = randomCount(i * 5 + 101, 2, 3);\n fanAngles(angle, 40, leafCount).forEach((leafAngle) => {\n addLink(level1, addNode(leafAngle, 50, NODE_DOT_MIN_RADIUS * 0.5), LEAF_LINK_WIDTH);\n });\n });\n}\n\nfunction buildRoot(index: number) {\n const { startAngle, endAngle } = SECTORS[index];\n const angle = (startAngle + endAngle) / 2;\n const usable = (endAngle - startAngle) * 0.8;\n const weight = ROOT_WEIGHTS[index];\n const leafDotRadius = NODE_DOT_MIN_RADIUS * 0.6;\n\n const root = addNode(angle, RING_RADIUS, NODE_DOT_MIN_RADIUS + weight * ROOT_DOT_RADIUS_RANGE);\n\n const popLevel1 = addNode(angle, POP_RADII[0], NODE_DOT_MIN_RADIUS + weight * LEVEL1_DOT_RADIUS_RANGE * 0.6);\n addLink(root, popLevel1, LINK_WIDTH, POP_LINK_OPACITY);\n const popLevel2Count = 2 + Math.round(weight * 2);\n fanAngles(angle, usable * 0.5, popLevel2Count).forEach((level2Angle, i) => {\n const popLevel2 = addNode(level2Angle, POP_RADII[1], NODE_DOT_MIN_RADIUS * 0.8);\n addLink(popLevel1, popLevel2, LINK_WIDTH, POP_LINK_OPACITY);\n const leafCount = randomCount(index * 31 + i * 7 + 211, 2, 3);\n fanAngles(level2Angle, (usable * 0.5) / popLevel2Count, leafCount).forEach((leafAngle) => {\n addLink(popLevel2, addNode(leafAngle, POP_RADII[2], leafDotRadius), LEAF_LINK_WIDTH, POP_LINK_OPACITY);\n });\n });\n\n const coreLevel1 = addNode(angle, CORE_RADII[0], NODE_DOT_MIN_RADIUS + weight * LEVEL1_DOT_RADIUS_RANGE);\n addLink(root, coreLevel1, LINK_WIDTH);\n const coreLevel2Count = 2 + Math.round(weight * 5);\n const coreLevel2Spread = usable * 0.75;\n const level2Slot = coreLevel2Spread / coreLevel2Count;\n fanAngles(angle, coreLevel2Spread, coreLevel2Count).forEach((level2Angle, i) => {\n const coreLevel2 = addNode(level2Angle, CORE_RADII[1], NODE_DOT_MIN_RADIUS * 0.9);\n addLink(coreLevel1, coreLevel2, LINK_WIDTH);\n const level3Count = randomCount(index * 31 + i * 7 + 1, 2, 5);\n fanAngles(level2Angle, level2Slot * 0.9, level3Count).forEach((level3Angle, j) => {\n const coreLevel3 = addNode(level3Angle, CORE_RADII[2], leafDotRadius);\n addLink(coreLevel2, coreLevel3, LEAF_LINK_WIDTH);\n if (weight < 0.75) return;\n const level4Count = randomCount(index * 131 + i * 17 + j * 3 + 401, 1, 2);\n fanAngles(level3Angle, (level2Slot * 0.9) / level3Count, level4Count).forEach((level4Angle) => {\n addLink(coreLevel3, addNode(level4Angle, CORE_RADII[3], leafDotRadius * 0.8), LEAF_LINK_WIDTH);\n });\n });\n });\n}\n\nbuildMainstream();\nfor (let i = 0; i < N_ROOTS; i++) buildRoot(i);\n\n// Square and centered on the wheel's pivot (like the real wheel) so the shimmer's rotation\n// origin stays at the viewBox's own middle.\nconst CONTENT_RADIUS = Math.max(\n RING_RADIUS,\n ...DOTS.map((dot) => Math.hypot(dot.point.x, dot.point.y) + dot.r),\n);\nconst CANVAS_SIZE = (CONTENT_RADIUS + CANVAS_PADDING) * 2;\nconst CANVAS_ORIGIN = -(CONTENT_RADIUS + CANVAS_PADDING);\n\n// Rendered as a CSS conic-gradient on a layer separate from the content SVG (see the component\n// below) rather than as SVG wedge paths sharing the content's own viewBox: the content SVG uses\n// `preserveAspectRatio=\"xMidYMid meet\"` so the wheel itself is never cropped, but \"meet\" letterboxes\n// a square viewBox inside a non-square container — a wedge drawn in that same coordinate space\n// would letterbox too, leaving the container's corners uncolored. A CSS gradient instead covers\n// `100% / 100%` of the container directly, independent of the content's aspect ratio.\n// CSS conic-gradient's 0deg (12 o'clock, clockwise) matches this file's own angle convention, so\n// stops need only be shifted so the first one starts at 0deg.\nconst SECTOR_GRADIENT_ROTATION = SECTORS[0].startAngle;\nconst SECTOR_GRADIENT_CSS = `conic-gradient(from ${SECTOR_GRADIENT_ROTATION}deg, ${SECTORS.map(\n (sector) =>\n `${sector.color} ${sector.startAngle - SECTOR_GRADIENT_ROTATION}deg ${\n sector.endAngle - SECTOR_GRADIENT_ROTATION\n }deg`,\n).join(\", \")})`;\n\n// The sweep is an HTML layer (rotating on the compositor) clipped by a static mask, instead of\n// an SVG <mask> re-rasterized every frame: SVG children can't be composited, so animating a wedge\n// under an SVG mask repaints hundreds of shapes per frame on the main thread, which is busiest\n// exactly while the skeleton is shown. The mask ships as wheel-skeleton-mask.svg (referenced from\n// styles.css, snapshot-tested against this function) so it applies from the first paint.\nexport function buildMaskSvgMarkup(): string {\n const links = LINKS.map(\n (link) =>\n `<path d=\"${link.d}\" fill=\"none\" stroke=\"#fff\" stroke-width=\"${link.width}\" stroke-opacity=\"${link.opacity}\" stroke-linecap=\"round\"/>`,\n ).join(\"\");\n const dots = DOTS.map((dot) => `<circle cx=\"${dot.point.x}\" cy=\"${dot.point.y}\" r=\"${dot.r}\" fill=\"#fff\"/>`).join(\"\");\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"${CANVAS_ORIGIN} ${CANVAS_ORIGIN} ${CANVAS_SIZE} ${CANVAS_SIZE}\">${links}${dots}</svg>`;\n}\n\n// Start time of the first skeleton's spin, shared so every later remount (one per loading stage)\n// continues at the same angle instead of restarting from 0deg.\nlet sharedStartTime: CSSNumberish | null = null;\n\nexport const WHEEL_SKELETON_SPIN_ANIMATION = \"gtv-wheel-skeleton-spin\";\n\nfunction adoptRunningSweepStartTime(): void {\n if (sharedStartTime !== null || typeof document === \"undefined\") return;\n const running = document.getAnimations?.().find(\n (animation) => (animation as CSSAnimation).animationName === WHEEL_SKELETON_SPIN_ANIMATION,\n );\n sharedStartTime = running?.startTime ?? null;\n}\n\nexport function GenreTreeWheelSkeleton() {\n // Read during render: a server-rendered skeleton discarded at hydration is already out of the\n // document by layout-effect time, and the replacement must continue its angle.\n adoptRunningSweepStartTime();\n // Unique per mount so multiple skeletons on one page don't collide on <defs> ids.\n const uid = useId().replace(/[^a-zA-Z0-9]/g, \"\");\n const shapesId = `genre-tree-wheel-skeleton-shapes-${uid}`;\n const mainstreamId = `genre-tree-wheel-skeleton-mainstream-${uid}`;\n const wedgeRef = useRef<HTMLDivElement>(null);\n\n // The mask and the spin come from CSS so the server-rendered skeleton is already correct before\n // hydration; this only syncs the phase. No animation means reduced motion or no WAAPI.\n useLayoutEffect(() => {\n const [animation] = wedgeRef.current?.getAnimations?.() ?? [];\n if (!animation) return;\n sharedStartTime ??= animation.startTime ?? document.timeline.currentTime;\n animation.startTime = sharedStartTime;\n }, []);\n\n return (\n <div className=\"gtv-wheel-skeleton\">\n <div\n className=\"gtv-wheel-skeleton-sector-layer\"\n style={{ background: SECTOR_GRADIENT_CSS, opacity: SECTOR_FILL_OPACITY }}\n aria-hidden=\"true\"\n />\n <span className=\"gtv-skeleton-sr-only\">Loading genre tree…</span>\n <svg\n viewBox={`${CANVAS_ORIGIN} ${CANVAS_ORIGIN} ${CANVAS_SIZE} ${CANVAS_SIZE}`}\n preserveAspectRatio=\"xMidYMid meet\"\n className=\"gtv-wheel-skeleton-svg\"\n aria-hidden=\"true\"\n >\n <defs>\n <radialGradient id={mainstreamId}>\n <stop offset=\"0%\" stopColor=\"#FFFFFF\" stopOpacity={0.45} />\n <stop offset=\"65%\" stopColor=\"#FFFFFF\" stopOpacity={0.15} />\n <stop offset=\"100%\" stopColor=\"#FFFFFF\" stopOpacity={0} />\n </radialGradient>\n <g id={shapesId}>\n {LINKS.map((link, i) => (\n <path\n key={`link-${i}`}\n d={link.d}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={link.width}\n strokeOpacity={link.opacity}\n strokeLinecap=\"round\"\n />\n ))}\n {DOTS.map((dot, i) => (\n <circle key={`dot-${i}`} cx={dot.point.x} cy={dot.point.y} r={dot.r} fill=\"currentColor\" />\n ))}\n </g>\n </defs>\n\n <circle cx={0} cy={0} r={RING_RADIUS} fill=\"#FFFFFF\" fillOpacity={INNER_TINT_OPACITY} />\n <circle cx={0} cy={0} r={MAINSTREAM_RADIUS} fill={`url(#${mainstreamId})`} />\n\n <use href={`#${shapesId}`} color={SHAPE_COLOR} />\n </svg>\n <div className=\"gtv-wheel-skeleton-shimmer\" aria-hidden=\"true\">\n <div ref={wedgeRef} className=\"gtv-wheel-skeleton-shimmer-wedge\" />\n </div>\n </div>\n );\n}\n","\"use client\";\n\n// Indent level and relative width per placeholder row — a few roots, one expanded, so it reads\n// as a nested outline rather than a flat list.\nconst ROWS: { depth: number; width: number }[] = [\n { depth: 0, width: 38 },\n { depth: 0, width: 26 },\n { depth: 1, width: 18 },\n { depth: 2, width: 30 },\n { depth: 2, width: 24 },\n { depth: 1, width: 16 },\n { depth: 0, width: 32 },\n { depth: 0, width: 22 },\n];\n\nexport function GenreTreeOutlineSkeleton() {\n return (\n <div className=\"gtv-skeleton gtv-outline-skeleton\">\n <span className=\"gtv-skeleton-sr-only\">Loading genre tree…</span>\n {ROWS.map((row, i) => (\n <div\n key={i}\n className=\"gtv-outline-skeleton-row\"\n aria-hidden=\"true\"\n style={{ marginLeft: `${row.depth * 1.25}rem`, width: `${row.width}%` }}\n />\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport type { GenreTreeViewMode } from \"./types\";\nimport { GenreTreeSkeleton } from \"./GenreTreeSkeleton\";\nimport { GenreTreeWheelSkeleton } from \"./GenreTreeWheelSkeleton\";\nimport { GenreTreeOutlineSkeleton } from \"./GenreTreeOutlineSkeleton\";\n\nexport type GenreTreeViewSkeletonProps = {\n viewMode: GenreTreeViewMode;\n};\n\n// Single source of truth for viewMode -> skeleton shape, so a consumer's next/dynamic\n// `loading` fallback (rendered before GenreTreeView's own chunk downloads and this logic\n// can run) never disagrees with what GenreTreeView renders internally once mounted.\nexport function GenreTreeViewSkeleton({ viewMode }: GenreTreeViewSkeletonProps) {\n if (viewMode === \"wheel\" || viewMode === \"pop-core\") {\n return (\n <div className=\"gtv-view-skeleton-wheel-wrapper\">\n <GenreTreeWheelSkeleton />\n </div>\n );\n }\n if (viewMode === \"outline\") return <GenreTreeOutlineSkeleton />;\n return <GenreTreeSkeleton />;\n}\n"],"mappings":"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","names":["import_react","ReactDOMServer","svgHeight","svgWidth","import_jsx_runtime","import_jsx_runtime","import_react","import_react","import_react","import_md","import_md","import_jsx_runtime","import_react","import_md","import_jsx_runtime","import_react","import_md","import_pi","import_jsx_runtime","import_jsx_runtime","itemCount","fill","import_react","import_md","import_jsx_runtime","import_jsx_runtime","import_react","d3","CHIP_GAP","import_jsx_runtime","d3","import_jsx_runtime","import_jsx_runtime","import_react","d3","sum","CHIP_GAP","right","collectSubtree","import_jsx_runtime","index","d3","import_jsx_runtime","import_react","d3","import_react","import_react","import_md","import_jsx_runtime","centerNodes","import_jsx_runtime","LANDING_ANGLE","computeContinuousAngles","d3","index","import_jsx_runtime","import_react","import_jsx_runtime","OutlineSection","import_react","import_jsx_runtime","CONNECTOR_COLOR","CORNER_RADIUS","import_react","import_jsx_runtime","sum","import_jsx_runtime","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/GenreTree.tsx","../src/NodeHelper.tsx","../src/constants.ts","../src/types.ts","../src/d3-helper/d3-grid-helper.ts","../src/d3-helper/d3-path-helper.ts","../src/tree-renderer.ts","../src/FloatingControls.tsx","../src/BusyIndicator.tsx","../src/use-pan-zoom.ts","../src/zoom-pan.ts","../src/use-node-info-panel.ts","../src/info-panel-geometry.ts","../src/use-tree-index.ts","../src/tree-index.ts","../src/InfoPanel.tsx","../src/InfoPanelExpandButton.tsx","../src/InfoPanelSection.tsx","../src/NodeToolbar.tsx","../src/OverlayPanel.tsx","../src/GenreTreeWheelBase.tsx","../src/root-grouping.ts","../src/wheel-geometry.ts","../src/GenreTreeWheel.tsx","../src/GenreTreeWheelRight.tsx","../src/GenreTreeWheelRadialBase.tsx","../src/defer-click.ts","../src/radial-wheel-geometry.ts","../src/pop-core-radial-layout.ts","../src/core-radial-layout.ts","../src/pop-core-split.ts","../src/GenreTreeWheelRadial.tsx","../src/GenreTreeWheelRadialPopCoreBase.tsx","../src/use-focus-history.ts","../src/WheelBreadcrumb.tsx","../src/pop-core-frame.ts","../src/GenreTreeWheelRadialPopCore.tsx","../src/GenreTreeOutline.tsx","../src/GenreTreeSkeleton.tsx","../src/GenreTreeWheelSkeleton.tsx","../src/GenreTreeOutlineSkeleton.tsx","../src/GenreTreeViewSkeleton.tsx"],"sourcesContent":["export { GenreTree } from \"./GenreTree\";\nexport { GenreTreeWheel } from \"./GenreTreeWheel\";\nexport { GenreTreeWheelRight } from \"./GenreTreeWheelRight\";\nexport { GenreTreeWheelRadial } from \"./GenreTreeWheelRadial\";\nexport { GenreTreeWheelRadialPopCore } from \"./GenreTreeWheelRadialPopCore\";\nexport { GenreTreeOutline } from \"./GenreTreeOutline\";\nexport { GenreTreeSkeleton } from \"./GenreTreeSkeleton\";\nexport { GenreTreeWheelSkeleton } from \"./GenreTreeWheelSkeleton\";\nexport { GenreTreeOutlineSkeleton } from \"./GenreTreeOutlineSkeleton\";\nexport { GenreTreeViewSkeleton } from \"./GenreTreeViewSkeleton\";\nexport { getGenreTreeColor, DEFAULT_FRAME_WIDTH, DEFAULT_FRAME_HEIGHT } from \"./constants\";\nexport { groupNodesByRoot } from \"./root-grouping\";\nexport { isZoomableNode } from \"./pop-core-frame\";\nexport type {\n GenreTreeNode,\n GenreTreeProps,\n GenreTreePlayState,\n GenreTreeViewMode,\n TreeOrientation,\n GenreTreeAction,\n WheelZoomMode,\n ControlsOrientation,\n ZoomControlsVisibility,\n GenreTreeOverlayPanel,\n GenreTreeViewport,\n InfoPanelBody,\n InfoPanelSections,\n} from \"./types\";\nexport type { GenreTreeViewSkeletonProps } from \"./GenreTreeViewSkeleton\";\nexport type { GenreTreeWheelProps } from \"./GenreTreeWheel\";\nexport type { GenreTreeWheelRightProps } from \"./GenreTreeWheelRight\";\nexport type { GenreTreeWheelRadialProps } from \"./GenreTreeWheelRadial\";\nexport type { GenreTreeWheelRadialPopCoreProps } from \"./GenreTreeWheelRadialPopCore\";\nexport type { FocusChangeReason } from \"./GenreTreeWheelRadialPopCoreBase\";\nexport type { GenreTreeOutlineProps } from \"./GenreTreeOutline\";\nexport type { GenreTreeRootGroup } from \"./root-grouping\";\n","\"use client\";\n\nimport { useEffect, useRef, useMemo } from \"react\";\nimport * as d3 from \"d3\";\n\nimport { GenreTreeProps } from \"./types\";\nimport { buildTreeHierarchyStructure } from \"./NodeHelper\";\nimport {\n calculateSvgDimensions,\n createTreeLayout,\n setupTreeLayout,\n renderTree,\n} from \"./tree-renderer\";\nimport {\n ACCENT_TEXT_COLOR,\n getGenreTreeColor,\n INFO_PANEL_WIDTH,\n TEXT_COLOR,\n tintSurface,\n ZOOM_FOCUS_SCALE,\n getItemCountRange,\n} from \"./constants\";\nimport { FloatingControls } from \"./FloatingControls\";\nimport { usePanZoom } from \"./use-pan-zoom\";\nimport { queryTreeContentElements } from \"./zoom-pan\";\nimport { useNodeInfoPanel } from \"./use-node-info-panel\";\nimport { useTreeIndex } from \"./use-tree-index\";\nimport { resolveInfoPanelObscuredArea } from \"./info-panel-geometry\";\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\n\n/**\n * Renders one connected hierarchy of `GenreTreeNode`s as an interactive D3/SVG tree.\n * Purely presentational: fetching, mutations, and popups are the consumer's responsibility,\n * wired through the callback props.\n */\nexport function GenreTree({\n nodes,\n className,\n rootColor,\n orientation = \"horizontal\",\n hideRoot = false,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n interactive = true,\n depthSpacingScale = 1,\n showToolbar = true,\n selectedNodeId: selectedNodeIdProp,\n renderInfoPanelBody,\n onSelectedNodeChange,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n itemCountScaleMax,\n nodeCountForSizing,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n wheelZoom,\n defaultViewport,\n onViewportChange,\n controlsOrientation = \"vertical\",\n showZoomControls = \"auto\",\n floatingControlsLeading,\n busy,\n}: GenreTreeProps) {\n const index = useTreeIndex(nodes);\n const svgRef = useRef<SVGSVGElement>(null);\n const viewportRef = useRef<HTMLDivElement>(null);\n const stageRef = useRef<HTMLDivElement>(null);\n const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });\n const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId: selectedNodeIdProp, onSelectedNodeChange });\n const itemCountRange = useMemo(\n () => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: nodeCountForSizing ?? nodes.length }),\n [nodes, itemCountScaleMax, nodeCountForSizing],\n );\n const showNodeInfoRef = useRef(showNodeInfo);\n useEffect(() => {\n showNodeInfoRef.current = showNodeInfo;\n });\n // Read via a ref inside the D3 render effect below rather than depending on `panZoom` directly —\n // its identity changes every render, which would otherwise re-run (and re-mount the whole D3\n // tree) on every pan/zoom, exactly what that effect's own dependency list is designed to avoid.\n const centerOnElementRef = useRef(panZoom.centerOnElement);\n useEffect(() => {\n centerOnElementRef.current = panZoom.centerOnElement;\n });\n\n const { treeData, resolvedRootColor, svgWidth, svgHeight } = useMemo(() => {\n const root = buildTreeHierarchyStructure(d3, nodes);\n const originalTreeData = createTreeLayout(\n d3,\n root,\n orientation,\n depthSpacingScale,\n itemCountRange,\n );\n const {\n svgWidth: width,\n svgHeight: height,\n highestVerticalCoordinate,\n rootDepthOffset,\n } = calculateSvgDimensions(\n d3,\n originalTreeData,\n orientation,\n hideRoot,\n depthSpacingScale,\n itemCountRange,\n );\n const reshapedTreeData = setupTreeLayout(\n d3,\n originalTreeData,\n highestVerticalCoordinate,\n orientation,\n rootDepthOffset,\n width,\n depthSpacingScale,\n itemCountRange,\n );\n\n return {\n treeData: reshapedTreeData,\n resolvedRootColor: rootColor ?? getGenreTreeColor(root.data.wikidataId),\n svgWidth: width,\n svgHeight: height,\n };\n }, [nodes, rootColor, orientation, hideRoot, depthSpacingScale, itemCountRange]);\n\n // A reparent-in-progress node can belong to a *different* GenreTree instance (a different\n // root). Only the tree that actually contains it needs to block self/descendants as targets.\n const reparentForbiddenIds = useMemo(() => {\n if (!reparentingNodeId) return [];\n const reparentingHierarchyNode = treeData\n .descendants()\n .find((d) => d.data.id === reparentingNodeId);\n if (!reparentingHierarchyNode) return [];\n return reparentingHierarchyNode.descendants().map((d) => d.data.id);\n }, [treeData, reparentingNodeId]);\n\n const effectiveSelectedNodeId =\n selectedNodeIdProp !== undefined ? selectedNodeIdProp : (panel?.node.id ?? null);\n\n useEffect(() => {\n if (!svgRef.current) return;\n\n d3.select(svgRef.current).selectAll(\"*\").remove();\n\n renderTree(\n d3,\n svgRef,\n treeData,\n svgWidth,\n svgHeight,\n reparentingNodeId,\n reparentForbiddenIds,\n resolvedRootColor,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId) {\n void onReparent?.(reparentingNodeId, newParentId);\n }\n },\n onNodeClick: (data, event) => {\n const clickedElement = event.currentTarget as Element | null;\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n event.currentTarget as Element | null,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n additionalActions,\n playingNodeId,\n playState,\n },\n orientation,\n hideRoot,\n effectiveSelectedNodeId,\n itemCountRange,\n );\n }, [\n treeData,\n svgWidth,\n svgHeight,\n reparentingNodeId,\n reparentForbiddenIds,\n resolvedRootColor,\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n orientation,\n hideRoot,\n effectiveSelectedNodeId,\n itemCountRange,\n ]);\n\n // Mirrors the onNodeClick/onSelectNode handling: an externally-controlled selection (e.g. from a\n // search result) centers the viewport on that node and opens its info panel exactly as a direct\n // click would. Declared after the draw effect above so the node's group element already exists,\n // and skipped when `interactive` is false since the panel then belongs to an ancestor.\n useEffect(() => {\n if (!interactive || !selectedNodeIdProp) return;\n if (panel?.node.id === selectedNodeIdProp) return;\n const targetNode = index.byId.get(selectedNodeIdProp);\n const element = svgRef.current?.querySelector(\n `#group-${CSS.escape(selectedNodeIdProp)}`,\n );\n if (!targetNode || !element) return;\n centerOnElementRef.current(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(targetNode, element, viewportRef.current, { notify: false });\n }, [interactive, selectedNodeIdProp, index, panel]);\n\n useEffect(() => {\n const svgElement = svgRef.current;\n return () => {\n if (svgElement) {\n d3.select(svgElement).selectAll(\"*\").remove();\n }\n };\n }, []);\n\n // Starts the view fit to the whole tree instead of at scale 1 / pan (0, 0) — runs once the\n // tree's own draw effect above has populated svgRef, guarded so it never re-fires afterward\n // (e.g. on a subsequent nodes/layout change) and fights the user's own pan/zoom.\n const hasInitialFitRef = useRef(false);\n useEffect(() => {\n if (hasInitialFitRef.current || !interactive) return;\n const elements = queryTreeContentElements(svgRef.current);\n if (elements.length === 0) return;\n hasInitialFitRef.current = true;\n panZoom.fitToFrame(elements, { silent: true });\n });\n\n const svg = (\n <svg\n ref={svgRef}\n width={svgWidth}\n height={svgHeight}\n style={{ overflow: \"visible\", display: \"block\" }}\n />\n );\n\n if (!interactive) {\n // No own viewport, transform, or controls — an ancestor (e.g. GenreTreeWheel's shared stage)\n // supplies the entire pan/zoom transform for this tree and whatever else it's paired with.\n return <div className={className}>{svg}</div>;\n }\n\n return (\n <div\n ref={viewportRef}\n className={className}\n aria-busy={busy}\n style={\n {\n position: \"relative\",\n overflow: \"hidden\",\n containerType: \"inline-size\",\n width: \"100%\",\n height: \"100%\",\n // Everything inside this div (the tree stage, the info panel, the floating zoom controls)\n // is absolutely positioned, so nothing in-flow gives it a size of its own — it requires an\n // explicit-sized ancestor for width/height: 100% to resolve, and without one collapses to\n // 0 height, below ZOOM_FIT_PADDING. usePanZoom's fitToFrame already no-ops\n // rather than fit into a viewport that small (see ZOOM_FIT_PADDING guard in use-pan-zoom.ts),\n // so no minWidth/minHeight floor is applied here — flooring at the tree's own size used to\n // force this div larger than a smaller explicit-sized ancestor for any tree bigger than it,\n // which broke fitToFrame's viewport measurement and left content clipped against that\n // ancestor's actual (smaller) visible bounds.\n cursor: \"grab\",\n // Never \"auto\": a touchscreen two-finger pinch would otherwise never reach JS at all — the\n // browser consumes it as native page zoom before usePanZoom's pointer handlers see it.\n touchAction: panZoom.touchAction,\n } as React.CSSProperties\n }\n onPointerDown={panZoom.handlePointerDown}\n >\n {/* transform-origin: 0 0 so panX/panY/zoomScale compose in one consistent coordinate\n system — see use-pan-zoom.ts. */}\n <div\n ref={stageRef}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n transform: panZoom.transform,\n transformOrigin: \"0 0\",\n }}\n >\n {svg}\n </div>\n {panel && (\n <InfoPanel\n node={panel.node}\n // Mirrors tree-renderer.ts's isSubtreeCore(d) = hideRoot && d.depth >= 1. panel.node is\n // only ever set from a click on a rendered node, and the root (depth 0) is excluded from\n // rendering entirely when hideRoot is true, so panel.node is guaranteed to have a parent\n // whenever hideRoot is true — leaving hideRoot as the only variable, as with the children\n // below.\n fill={hideRoot ? resolvedRootColor : tintSurface(resolvedRootColor)}\n textColor={hideRoot ? ACCENT_TEXT_COLOR : TEXT_COLOR}\n parentNode={\n index.byId.get(panel.node.parentId ?? \"\")\n ? {\n node: index.byId.get(panel.node.parentId!)!,\n fill: hideRoot\n ? resolvedRootColor\n : tintSurface(resolvedRootColor),\n textColor: hideRoot ? ACCENT_TEXT_COLOR : TEXT_COLOR,\n }\n : null\n }\n childNodes={(index.childrenByParentId.get(panel.node.id) ?? [])\n .map((n) => ({\n node: n,\n // Mirrors tree-renderer.ts's isSubtreeCore(d) = hideRoot && d.depth >= 1 — every\n // direct child here has a parent, so depth >= 1 always holds, leaving hideRoot as\n // the only variable.\n fill: hideRoot\n ? resolvedRootColor\n : tintSurface(resolvedRootColor),\n textColor: hideRoot ? ACCENT_TEXT_COLOR : TEXT_COLOR,\n }))}\n ancestorNodes={index.ancestorsOf(panel.node.id).map(\n (n): InfoPanelChild => ({\n node: n,\n // Same fill/textColor expression as parentNode/childNodes above, including for\n // the root itself when the chain reaches it.\n fill: hideRoot ? resolvedRootColor : tintSurface(resolvedRootColor),\n textColor: hideRoot ? ACCENT_TEXT_COLOR : TEXT_COLOR,\n }),\n )}\n side={panel.side}\n onClose={closeNodeInfo}\n onSelectNode={(id) => {\n const targetNode = index.byId.get(id)!;\n const element = svgRef.current!.querySelector(\n `#group-${CSS.escape(id)}`,\n );\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(\n targetNode,\n element ?? viewportRef.current,\n viewportRef.current,\n );\n }}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n renderInfoPanelBody={renderInfoPanelBody}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n }\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n <FloatingControls\n panZoom={panZoom}\n orientation={controlsOrientation}\n zoomControls={showZoomControls}\n leading={floatingControlsLeading}\n busy={busy}\n onFit={() => panZoom.fitToFrame(queryTreeContentElements(svgRef.current))}\n />\n </div>\n );\n}\n","import ReactDOMServer from \"react-dom/server\";\nimport { PiGraphFill } from \"react-icons/pi\";\nimport * as d3 from \"d3\";\n\nimport { GenreTreeNode } from \"./types\";\nimport {\n ACCENT_COLOR,\n ACCENT_TEXT_COLOR,\n CORNER_RADIUS,\n calculateNodeDimensions,\n Dimensions,\n getItemCountRange,\n ItemCountRange,\n} from \"./constants\";\n\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\n\n/** Builds the hierarchical tree structure from a flat node list. Each node's `itemCount` is\n * already its subtree total (see `GenreTreeNode.itemCount`), so it is used as-is, never summed. */\nexport function buildTreeHierarchyStructure(d3Lib: typeof import(\"d3\"), nodes: GenreTreeNode[]) {\n return d3Lib\n .stratify<GenreTreeNode>()\n .id((d) => d.id)\n .parentId((d) => d.parentId)(nodes);\n}\n\n/** A subtree root's count is the total of the whole subtree, so it's always that\n * subtree's own local maximum — meaning tree-renderer always sizes a hidden root against its own\n * subtree's item-count range, never the range a caller might be using to compare that root\n * against sibling roots elsewhere (e.g. a wheel's cross-root chip-sizing scale). Callers that need\n * to predict a hidden root's rendered width/height — to reserve matching clearance for it — must\n * mirror this same local-range computation rather than reusing their own cross-root\n * scale. */\nexport function calculateLocalRootDimensions(\n d3Lib: typeof import(\"d3\"),\n nodes: GenreTreeNode[],\n options: { itemCountScaleMax?: number; nodeCountForSizing?: number } = {},\n): Dimensions {\n const root = buildTreeHierarchyStructure(d3Lib, nodes);\n const range = getItemCountRange(root.descendants().map((d) => d.data), options);\n return calculateNodeDimensions(root.data.itemCount, range);\n}\n\n/** A wheel's tree-anchor clearance along one axis: how far past its own wheel-anchor point a\n * hidden root's mounted subtree needs to be pushed so its root->depth1 links stop at the visible\n * chip's outer edge instead of drawing across it. Two half-extents stack, not one — the hidden\n * root's own local half-extent (its link endpoint sits at its own invisible box's *center*, per\n * appendPaths' `d.x + WIDTH/2` convention, so clearing the box at all takes a full local\n * half-extent) plus the visible chip's own cross-root display half-extent (clearing the box only\n * reaches the chip's *center*, since that's where the anchor and the chip are both pinned — a\n * second half-extent, this time the chip's own displayed size, is what reaches its actual edge). */\nexport function calculateRootAnchorClearance(\n d3Lib: typeof import(\"d3\"),\n nodes: GenreTreeNode[],\n rootItemCount: number,\n rootItemCountRange: ItemCountRange,\n axis: keyof Dimensions,\n options: { itemCountScaleMax?: number; nodeCountForSizing?: number } = {},\n): number {\n const localExtent = calculateLocalRootDimensions(d3Lib, nodes, options)[axis];\n const displayExtent = calculateNodeDimensions(rootItemCount, rootItemCountRange)[axis];\n return (localExtent + displayExtent) / 2;\n}\n\n/** Adds the \"select as new parent\" overlay to a node while a reparent is in progress. */\nexport function addReparentTargetOverlay(\n d3Lib: typeof import(\"d3\"),\n parentNode: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n onReparentTargetSelect: (newParentId: string) => void,\n itemCountRange: ItemCountRange,\n) {\n const nodeId = (parentNode.datum() as D3Node).data.id;\n const nodeData = (parentNode.datum() as D3Node).data;\n const dimensions = calculateNodeDimensions(nodeData.itemCount, itemCountRange);\n\n let overlayGroup = parentNode.select<SVGGElement>(\"#select-as-new-parent-group-\" + nodeId);\n if (overlayGroup.empty()) {\n overlayGroup = parentNode\n .append(\"g\")\n .attr(\"id\", \"select-as-new-parent-group-\" + nodeId)\n .style(\"cursor\", \"pointer\")\n .on(\"mouseleave\", function () {\n d3Lib.select(this).remove();\n });\n\n overlayGroup\n .append(\"rect\")\n .attr(\"width\", dimensions.WIDTH)\n .attr(\"height\", dimensions.HEIGHT)\n .attr(\"x\", -dimensions.WIDTH / 2)\n .attr(\"y\", -dimensions.HEIGHT / 2)\n .attr(\"rx\", CORNER_RADIUS)\n .attr(\"ry\", CORNER_RADIUS)\n .attr(\"fill\", ACCENT_COLOR);\n\n overlayGroup\n .append(\"foreignObject\")\n .attr(\"width\", dimensions.WIDTH)\n .attr(\"height\", dimensions.HEIGHT)\n .attr(\"x\", -dimensions.WIDTH / 2)\n .attr(\"y\", -dimensions.HEIGHT / 2)\n .html(() =>\n ReactDOMServer.renderToString(\n <div className=\"gtv-reparent-target\">\n <PiGraphFill size={20} color={ACCENT_TEXT_COLOR} />\n <div className=\"gtv-reparent-target-label\" style={{ color: ACCENT_TEXT_COLOR }}>\n Select as new parent\n </div>\n </div>,\n ),\n )\n .on(\"click\", function (_event: MouseEvent, d: unknown) {\n onReparentTargetSelect((d as D3Node).data.id);\n });\n }\n\n return overlayGroup;\n}\n","import { hsl } from \"d3\";\nimport type { TreeIndex } from \"./tree-index\";\n\n// Visual theme tokens. All render code (tree-renderer.ts, d3-path-helper.ts,\n// NodeHelper.tsx) reads exclusively from this block, so the tree's whole look can be\n// swapped by editing only these values.\nexport const SURFACE_FILL = \"#FFFFFF\";\nexport const SURFACE_BORDER_COLOR = \"#E4E4E7\";\nexport const CORNER_RADIUS = 8;\nexport const ELEVATION = false;\n\nexport const CONNECTOR_COLOR = \"#D4D4D8\";\nexport const CONNECTOR_OPACITY = 1;\n\n// Radial wheel subtree links (renderPopSubtree) take a muted tintSurface of their root's color;\n// pop links are lighter than core ones, matching the pop/core node fill contrast. The white\n// Mainstream Pop center uses TEXT_MUTED_COLOR instead.\nexport const RADIAL_CORE_LINK_TINT_RATIO = 0.5;\nexport const RADIAL_POP_LINK_TINT_RATIO = 0.3;\n\nexport const TEXT_COLOR = \"#18181B\";\nexport const TEXT_MUTED_COLOR = \"#A1A1AA\";\n\nexport const ACCENT_COLOR = \"#4F46E5\";\nexport const ACCENT_TEXT_COLOR = \"#FFFFFF\";\n\n// The selected node (the one whose info panel is open) gets a heavier accent-colored border,\n// while every other node dims (see .gtv-node--dimmed in styles.css) — reuses ACCENT_COLOR\n// rather than a separate color so the highlight reads as the same \"accent\" language as the\n// reparent-target overlay (NodeHelper.tsx).\nexport const SELECTED_BORDER_WIDTH_RATIO = 2.5;\n\n// Tokens for the toolbar's inline icon row and its overflow menu — reuse the surface/text/\n// accent tokens above so both read as part of the same light, neutral card language.\nexport const DANGER_COLOR = \"#DC2626\";\nexport const MENU_ROW_HOVER_FILL = \"#F4F4F5\";\nexport const DANGER_ROW_HOVER_FILL = \"rgba(220, 38, 38, 0.08)\";\nexport const ACCENT_TINT_FILL = \"rgba(79, 70, 229, 0.08)\";\n\nexport const TOOLBAR_GAP = 2;\n\nexport const MENU_WIDTH = 130;\n\nexport const PER_TREE_ACCENT_DOT = true;\nexport const ACCENT_DOT_SIZE = 4;\n\n// Portion of a tree's root color mixed into SURFACE_FILL for its nodes' background tint (see\n// tintSurface) — matches ACCENT_TINT_FILL's 0.08 alpha above so both read as the same \"subtle\n// wash\" strength.\nexport const ROOT_TINT_RATIO = 0.08;\n\n// Tint ratio for a radial wheel's pop-sector nodes (see renderPopSubtree in\n// pop-core-radial-layout.ts) — stronger than ROOT_TINT_RATIO so pop nodes read as a lighter\n// shade of their root's own color rather than the near-white wash the toolbar/card surfaces use.\nexport const POP_SECTOR_TINT_RATIO = 0.8;\n\n// Share of its color a node in an underground-scene subtree keeps (the rest is mixed toward black).\nexport const UNDERGROUND_DARKEN_RATIO = 0.62;\n\n// Opacity of the darker background slice drawn behind an underground-scene subtree, layered over\n// the root's own sector wash.\nexport const UNDERGROUND_SECTOR_FILL_OPACITY = 0.15;\n\n// Outer radius (px) of that slice — matches the half-size of the 200000px root sector wash, so it\n// reads as running out to infinity.\nexport const SCENE_SECTOR_OUTER_RADIUS = 100000;\n\nexport const DEFAULT_NODE_COLOR = \"#4F46E5\";\n\n// The pop-core renderers (GenreTreeWheelRadialPopCore's wheel pivot, GenreTreeOutline's first\n// entry) single out this specific root by name and throw if it's missing.\nexport const CENTER_NODE_NAME = \"Mainstream Pop\";\n\n// Placeholder palette cycled where no real root exists (GenreTreeWheelSkeleton); real roots use\n// ROOT_GENRE_COLORS.\nexport const TREE_COLORS = [\n \"#4F46E5\", // Indigo\n \"#0D9488\", // Teal\n \"#D97706\", // Amber\n \"#E11D48\", // Rose\n \"#7C3AED\", // Violet\n \"#0891B2\", // Cyan\n \"#059669\", // Emerald\n \"#EA580C\", // Orange\n];\n\n/** Blends `hex` into SURFACE_FILL at `ratio` (0-1) and returns an opaque hex color — used for a\n * node's root-color background tint. Opaque (rather than an rgba fill) so the tint reads the\n * same regardless of what the host page renders behind the tree. */\nexport function tintSurface(hex: string, ratio: number = ROOT_TINT_RATIO): string {\n const channel = (offset: number) => parseInt(hex.slice(offset, offset + 2), 16);\n const mix = (value: number) => Math.round(value * ratio + 255 * (1 - ratio));\n const toHex = (value: number) => value.toString(16).padStart(2, \"0\");\n return `#${toHex(mix(channel(1)))}${toHex(mix(channel(3)))}${toHex(mix(channel(5)))}`;\n}\n\n// Opacity of a wheel view's per-root background sector fill (see buildWheelSectorGradient /\n// buildSectorClipPathPolygon) — light enough to read as a wash behind chips/dividers/subtrees\n// rather than competing with them for attention.\nexport const ROOT_SECTOR_FILL_OPACITY = 0.15;\n\n/** Converts an opaque \"#RRGGBB\" color (e.g. a ROOT_GENRE_COLORS entry) to an \"rgba()\" string at\n * `opacity` (0-1) — unlike tintSurface, which stays opaque by blending toward a fixed background,\n * this produces a true transparent fill for layering over arbitrary content (e.g. a wheel's\n * sector-fill wash, which sits behind chips/dividers/subtrees of varying color). */\nexport function hexToRgba(hex: string, opacity: number): string {\n const channel = (offset: number) => parseInt(hex.slice(offset, offset + 2), 16);\n return `rgba(${channel(1)}, ${channel(3)}, ${channel(5)}, ${opacity})`;\n}\n\n// Fixed per-root colors keyed by the root's wikidata id (a QID or a synthetic \"LOCAL:\" key), so a\n// genre keeps its color across environments and renames. Mainstream Pop (Q37073) is deliberately\n// absent: the pop-core layouts render it white at the center.\nexport const ROOT_GENRE_COLORS: Readonly<Record<string, string>> = {\n \"LOCAL:blues-rock\": \"#DC2626\", // Red\n \"LOCAL:reggae-dub\": \"#16A34A\", // Green\n \"LOCAL:disco-funk\": \"#7C3AED\", // Violet\n Q11401: \"#B8860B\", // Hip-hop, deep gold\n Q9778: \"#2563EB\", // Electronic, blue\n Q1583807: \"#92400E\", // Art music (classical), brown\n Q8341: \"#CA8A04\", // Jazz, mustard\n Q45981: \"#4F46E5\", // Rhythm and blues, indigo\n Q43343: \"#0D9488\", // Folk, teal\n Q54365: \"#0891B2\", // Ska, cyan\n Q1640319: \"#475569\", // Experimental, slate\n};\n\nexport const FALLBACK_ROOT_COLOR = \"#6B7280\";\n\n/** Maps a root node's wikidata id to its fixed color, or FALLBACK_ROOT_COLOR when unmapped/absent. */\nexport function getGenreTreeColor(wikidataId: string | null | undefined): string {\n return (wikidataId && ROOT_GENRE_COLORS[wikidataId]) || FALLBACK_ROOT_COLOR;\n}\n\n// A zoomed view's ring roots are descendants of a top-level root, so they take a variation of the\n// color of the sector they were zoomed from: hue spread across +-this many degrees over siblings.\nconst CHILD_HUE_SPREAD_DEG = 30;\nconst CHILD_LIGHTNESS_STEP = 0.06;\nconst CHILD_MIN_LIGHTNESS = 0.25;\nconst CHILD_MAX_LIGHTNESS = 0.6;\n\n/** Color of `nodeId`: a top-level root's fixed color, otherwise a variation of its parent's color\n * (hue spread and alternating lightness by position among its non-pop siblings). A pure function of\n * the full tree, so a genre keeps its color at every zoom depth. */\nexport function getGenreNodeColor(index: Pick<TreeIndex, \"byId\" | \"childrenByParentId\">, nodeId: string): string {\n const node = index.byId.get(nodeId);\n if (!node) return FALLBACK_ROOT_COLOR;\n if (node.parentId === null) return getGenreTreeColor(node.wikidataId);\n const siblings = (index.childrenByParentId.get(node.parentId) ?? []).filter((sibling) => sibling.side !== \"pop\");\n const position = Math.max(0, siblings.findIndex((sibling) => sibling.id === nodeId));\n const base = hsl(getGenreNodeColor(index, node.parentId));\n const hueOffset =\n siblings.length > 1 ? -CHILD_HUE_SPREAD_DEG + (2 * CHILD_HUE_SPREAD_DEG * position) / (siblings.length - 1) : 0;\n const lightness = base.l + (position % 2 === 0 ? -CHILD_LIGHTNESS_STEP : CHILD_LIGHTNESS_STEP);\n return hsl(\n base.h + hueOffset,\n base.s,\n Math.min(CHILD_MAX_LIGHTNESS, Math.max(CHILD_MIN_LIGHTNESS, lightness)),\n ).formatHex();\n}\n\ntype GenreStyleIndex = Pick<TreeIndex, \"byId\" | \"childrenByParentId\" | \"ancestorsOf\">;\n\n/** Whether `nodeId` is the center \"Mainstream Pop\" root or one of its descendants. */\nexport function isMainstreamPopLineage(index: GenreStyleIndex, nodeId: string): boolean {\n const topRoot = index.ancestorsOf(nodeId)[0] ?? index.byId.get(nodeId);\n return topRoot?.name === CENTER_NODE_NAME;\n}\n\n/** Mixes `hex` toward black: `ratio` 1 keeps it, 0 is black. Counterpart of `tintSurface`. */\nexport function shadeSurface(hex: string, ratio: number): string {\n const channel = (offset: number) => parseInt(hex.slice(offset, offset + 2), 16);\n const toHex = (value: number) => Math.round(value * ratio).toString(16).padStart(2, \"0\");\n return `#${toHex(channel(1))}${toHex(channel(3))}${toHex(channel(5))}`;\n}\n\n/** Fill/text color of `nodeId` in the pop-core wheel: the Mainstream Pop lineage is a white tint\n * with dark text; any other genre is its own `getGenreNodeColor`, tinted when it sits on a pop\n * branch. Like `getGenreNodeColor`, independent of the zoom frame the node renders in. */\nexport function getGenreNodeStyle(index: GenreStyleIndex, nodeId: string): { fill: string; textColor: string } {\n if (isMainstreamPopLineage(index, nodeId)) {\n return { fill: tintSurface(\"#ffffff\", POP_SECTOR_TINT_RATIO), textColor: TEXT_COLOR };\n }\n const node = index.byId.get(nodeId);\n const lineageBelowTopRoot = node ? [...index.ancestorsOf(nodeId), node].slice(1) : [];\n const isPop = lineageBelowTopRoot.some((n) => n.side === \"pop\");\n const color = getGenreNodeColor(index, nodeId);\n return { fill: isPop ? tintSurface(color, POP_SECTOR_TINT_RATIO) : color, textColor: ACCENT_TEXT_COLOR };\n}\n\nexport interface Dimensions {\n WIDTH: number;\n HEIGHT: number;\n}\n\nexport const RECT_BASE_DIMENSIONS: Dimensions = {\n WIDTH: 180,\n HEIGHT: 30,\n};\n\n// Dynamic sizing configuration\nexport const MIN_NODE_WIDTH = 180;\nexport const MAX_NODE_WIDTH = 700;\nexport const MIN_NODE_HEIGHT = 35;\nexport const MAX_NODE_HEIGHT = 120;\n/** At this many nodes the current maximum card/dot sizes are retained. Above it, the available\n * size range shrinks in inverse proportion to node count, with the existing minimum as a floor. */\nexport const NODE_SIZE_REFERENCE_COUNT = 100;\n/** High-density trees keep their largest node at least 50% above the configured minimum. */\nexport const HIGH_DENSITY_NODE_MAX_FLOOR_SCALE = 1.5;\n/** Upper radius cap for radial nodes in sparse trees. */\nexport const LOW_DENSITY_DOT_MAX_RADIUS = 70;\n\n// Reserved space to a node's right/top/bottom for its toolbar icon row and overflow menu,\n// so the tree layout leaves room for them instead of packing nodes edge-to-edge. Width covers\n// the overflow menu's own width plus the gap the toolbar renders it at (see the `+ 4` x-offset\n// by the former hover toolbar), since the menu is wider than the icon row itself.\nexport const TOOLBAR_MENU_X_GAP = 4;\nexport const ACTIONS_OVERLAY_WIDTH = MENU_WIDTH + TOOLBAR_MENU_X_GAP;\nexport const ACTIONS_OVERLAY_HEIGHT = RECT_BASE_DIMENSIONS.HEIGHT * 7;\n\n// Layout slot size, not a rendered node's actual size (see calculateNodeDimensions) — every\n// node's slot has to fit the largest a node can render at, or a high-itemCount node overflows\n// its neighbors' slots and overlaps them.\nexport const NODE_DIMENSIONS: Dimensions = {\n WIDTH: MAX_NODE_WIDTH + ACTIONS_OVERLAY_WIDTH,\n HEIGHT: MAX_NODE_HEIGHT,\n};\n\nexport const HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES = 5;\nexport const VERTICAL_SEPARATION_BETWEEN_RECTANGLES = 20;\n// 0.8x: depth-axis spacing only (see HORIZONTAL_SEPARATION_BETWEEN_NODES and\n// VERTICAL_ORIENTATION_DEPTH_SEPARATION below), tightened 20% at the consumer's request.\nexport const DEPTH_AXIS_SPACING_FACTOR = 0.8;\nexport const HORIZONTAL_SEPARATION_BETWEEN_NODES =\n (NODE_DIMENSIONS.WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES) * DEPTH_AXIS_SPACING_FACTOR;\nexport const VERTICAL_SEPARATION_BETWEEN_NODES = NODE_DIMENSIONS.HEIGHT + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;\n\n// The depth-axis step for a vertical-orientation tree (root-to-leaf growth). Kept distinct from\n// VERTICAL_SEPARATION_BETWEEN_NODES, which horizontal-orientation trees also reuse as their\n// breadth (sibling) axis step — doubling that constant directly would have also doubled sibling\n// spacing in horizontal trees, not just the gap between depths.\nexport const VERTICAL_ORIENTATION_DEPTH_SEPARATION = VERTICAL_SEPARATION_BETWEEN_NODES * 2 * DEPTH_AXIS_SPACING_FACTOR;\n\n// Same-depth siblings in a vertical-orientation tree sit side by side on-screen, so this axis\n// doesn't need HORIZONTAL_SEPARATION_BETWEEN_NODES's baked-in toolbar/menu headroom — that's\n// only for the depth axis, where a hovered node's toolbar pops into the space before the next\n// generation. Reserving that much between every sibling pair made the wheel's tree read far\n// more spread out than its cards actually are.\nexport const SIBLING_SEPARATION_BETWEEN_NODES = MAX_NODE_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES;\n\n// Utility functions for dynamic node sizing\nexport interface ItemCountRange {\n min: number;\n max: number;\n /** Density-adjusted visual caps. Omitted by hand-built ranges, which retain the legacy caps. */\n maxNodeWidth?: number;\n maxNodeHeight?: number;\n maxDotRadius?: number;\n}\n\nexport interface ItemCountRangeOptions {\n /** Fixed upper bound for the metric (for example, total songs or views after filtering). */\n itemCountScaleMax?: number;\n /** Full dataset size for the density cap; defaults to the number of nodes in this range. */\n nodeCountForSizing?: number;\n}\n\nfunction getDensityAdjustedMax(baseMax: number, min: number, nodeCount: number): number {\n const safeNodeCount = Number.isFinite(nodeCount) ? Math.max(1, nodeCount) : 1;\n const availableRangeScale = Math.min(1, NODE_SIZE_REFERENCE_COUNT / safeNodeCount);\n const denseTreeFloor = min * HIGH_DENSITY_NODE_MAX_FLOOR_SCALE;\n return denseTreeFloor + (baseMax - denseTreeFloor) * availableRangeScale;\n}\n\nfunction getDensityAdjustedDotMax(nodeCount: number): number {\n const safeNodeCount = Number.isFinite(nodeCount) ? Math.max(1, nodeCount) : 1;\n const inverseDensityRadius = NODE_DOT_MAX_RADIUS * NODE_SIZE_REFERENCE_COUNT / safeNodeCount;\n const denseTreeFloor = NODE_DOT_MIN_RADIUS * HIGH_DENSITY_NODE_MAX_FLOOR_SCALE;\n return Math.max(denseTreeFloor, Math.min(LOW_DENSITY_DOT_MAX_RADIUS, inverseDensityRadius));\n}\n\n/** Builds the metric domain and visual size caps shared by a rendered tree. When a metric total\n * is supplied, counts map against [0, total] so filtering changes their relative sizes. The\n * maximum visual size also responds to the full node count: above NODE_SIZE_REFERENCE_COUNT, the\n * available range above the minimum shrinks in inverse proportion to node count. */\nexport function getItemCountRange(\n nodes: Array<{ itemCount: number }>,\n options: ItemCountRangeOptions = {},\n): ItemCountRange {\n const counts = nodes.map((node) => node.itemCount);\n const validScaleMax =\n options.itemCountScaleMax !== undefined &&\n Number.isFinite(options.itemCountScaleMax) &&\n options.itemCountScaleMax >= 0;\n const nodeCount = options.nodeCountForSizing ?? nodes.length;\n const observedMin = counts.length === 0 ? 0 : Math.min(...counts);\n const observedMax = counts.length === 0 ? 0 : Math.max(...counts);\n const includeDensityCaps = options.nodeCountForSizing !== undefined || validScaleMax;\n\n return {\n min: validScaleMax ? 0 : observedMin,\n max: validScaleMax ? options.itemCountScaleMax! : observedMax,\n ...(includeDensityCaps && {\n maxNodeWidth: getDensityAdjustedMax(MAX_NODE_WIDTH, MIN_NODE_WIDTH, nodeCount),\n maxNodeHeight: getDensityAdjustedMax(MAX_NODE_HEIGHT, MIN_NODE_HEIGHT, nodeCount),\n maxDotRadius: getDensityAdjustedDotMax(nodeCount),\n }),\n };\n}\n\n/** Log-scaled position of itemCount within `range`, normalized to [0, 1] — the node with the\n * lowest itemCount in range always sits at 0 and the highest always at 1, regardless of the\n * actual counts involved. A degenerate range (every node sharing one count) falls back to 0.\n * Real track counts commonly span tens to tens of thousands: a linear scale would flatten\n * nearly every node down near the minimum and let only the very largest stand out, so position\n * is computed on a log scale instead — equal *ratios* of item count grow a node by roughly\n * equal amounts, not equal absolute differences. log1p (not log) so an itemCount of 0 is finite. */\nexport function logarithmicPosition(itemCount: number, range: ItemCountRange): number {\n const { min, max } = range;\n const logMin = Math.log1p(Math.max(min, 0));\n const logMax = Math.log1p(Math.max(max, 0));\n const logValue = Math.log1p(Math.max(itemCount, 0));\n return logMax > logMin ? Math.max(0, Math.min(1, (logValue - logMin) / (logMax - logMin))) : 0;\n}\n\n/** Maps itemCount's log-scaled position within `range` onto [MIN_NODE_*, MAX_NODE_*]. */\nexport function calculateNodeDimensions(itemCount: number, range: ItemCountRange): Dimensions {\n const t = logarithmicPosition(itemCount, range);\n\n return {\n WIDTH: Math.round(MIN_NODE_WIDTH + t * ((range.maxNodeWidth ?? MAX_NODE_WIDTH) - MIN_NODE_WIDTH)),\n HEIGHT: Math.round(MIN_NODE_HEIGHT + t * ((range.maxNodeHeight ?? MAX_NODE_HEIGHT) - MIN_NODE_HEIGHT)),\n };\n}\n\nexport const NODE_MIN_BORDER_WIDTH = 1;\nexport const NODE_MAX_BORDER_WIDTH = 4;\n\n/** Border width of a node (card or dot), on the same log scale as its size; the selected node's\n * is SELECTED_BORDER_WIDTH_RATIO times its own width, so a selected small node can still be\n * thinner than an unselected large one. */\nexport function calculateNodeBorderWidth(itemCount: number, range: ItemCountRange, selected = false): number {\n const width =\n NODE_MIN_BORDER_WIDTH + logarithmicPosition(itemCount, range) * (NODE_MAX_BORDER_WIDTH - NODE_MIN_BORDER_WIDTH);\n return selected ? width * SELECTED_BORDER_WIDTH_RATIO : width;\n}\n\nexport const SELECTED_BORDER_TO_RADIUS_RATIO = 0.2;\n\n/** Selected-node border of a dot or chip, proportional to its radius. */\nexport function calculateSelectedNodeBorderWidth(radius: number): number {\n return radius * SELECTED_BORDER_TO_RADIUS_RATIO;\n}\n\nexport const LINK_TO_NODE_HEIGHT_RATIO = 1 / 6;\n\n/** Stroke width of the link leading to a node: a fixed fraction of that node's rendered height,\n * so a branch stays the same order of magnitude as the chip it leads to. */\nexport function calculateLinkWidth(itemCount: number, range: ItemCountRange): number {\n return calculateNodeDimensions(itemCount, range).HEIGHT * LINK_TO_NODE_HEIGHT_RATIO;\n}\n\n// The node-height fraction its font size renders at, interpolated by the same log-scaled t as\n// calculateNodeDimensions — small nodes get a smaller fraction of their (already small) height so\n// their label doesn't dominate the card, while the largest nodes reach a full half.\nconst MIN_NODE_FONT_HEIGHT_RATIO = 0.3;\nconst MAX_NODE_FONT_HEIGHT_RATIO = 0.5;\n\n/** A fraction of the node's own box height — keeps the label's font size proportional to its\n * node's rendered size, rather than interpolated separately over its own min/max range. */\nexport function calculateNodeFontSize(itemCount: number, range: ItemCountRange): number {\n const t = logarithmicPosition(itemCount, range);\n const { HEIGHT } = calculateNodeDimensions(itemCount, range);\n const ratio = MIN_NODE_FONT_HEIGHT_RATIO + t * (MAX_NODE_FONT_HEIGHT_RATIO - MIN_NODE_FONT_HEIGHT_RATIO);\n\n return Math.round(HEIGHT * ratio);\n}\n\n/** Layout-unit font size that renders a dot label at its on-screen size (LABEL_SCREEN_*_PX, log\n * scale like calculateNodeFontSize) under `zoomScale`. */\nexport function calculateLabelFontSizeAtZoom(itemCount: number, range: ItemCountRange, zoomScale: number): number {\n const t = logarithmicPosition(itemCount, range);\n return (LABEL_SCREEN_MIN_PX + t * (LABEL_SCREEN_MAX_PX - LABEL_SCREEN_MIN_PX)) / zoomScale;\n}\n\n/** A dot label's map-style importance: its log-scaled itemCount, lowered by\n * LABEL_DEPTH_RANK_PENALTY per level below the first under its root (`depth` 1), so a level is\n * named before the levels under it unless a deeper genre is much bigger. */\nexport function calculateLabelRank(itemCount: number, range: ItemCountRange, depth: number): number {\n return logarithmicPosition(itemCount, range) - Math.max(0, depth - 1) * LABEL_DEPTH_RANK_PENALTY;\n}\n\n/** Whether a label of `rank` (see calculateLabelRank) is named at `zoomScale` (see\n * LABEL_MIN_RANK_AT_MIN_ZOOM); the threshold falls linearly in log-zoom from ZOOM_MIN_SCALE to\n * ZOOM_FOCUS_SCALE, and below 0 past it so deep levels keep appearing while zooming further in. */\nexport function isLabelRankedForZoom(rank: number, zoomScale: number): boolean {\n const zoomOut = Math.log(ZOOM_FOCUS_SCALE / zoomScale) / Math.log(ZOOM_FOCUS_SCALE / ZOOM_MIN_SCALE);\n return rank >= Math.min(1, zoomOut) * LABEL_MIN_RANK_AT_MIN_ZOOM;\n}\n\n/** Root chip label size: its own layout size, floored so it never renders below\n * ROOT_LABEL_MIN_SCREEN_PX on screen — every root stays named at any zoom. */\nexport function calculateRootLabelFontSize(layoutFontSize: number, zoomScale: number): number {\n return Math.max(layoutFontSize, ROOT_LABEL_MIN_SCREEN_PX / zoomScale);\n}\n\nexport const NODE_DOT_MIN_RADIUS = 3;\nexport const NODE_DOT_MAX_RADIUS = 24;\nexport const NODE_DOT_LABEL_GAP = 6;\n// Half-width every node reserves on its ring when the radial wheels measure a subtree's angular\n// width: identical for all nodes, so layout depends on tree topology only and never on the size\n// metric (itemCount) that drives how big a dot actually renders.\nexport const NODE_LAYOUT_SLOT_RADIUS = 8;\n\n/** Radius of a radial-wheel subgenre dot, on the same log scale as its label's font size\n * (calculateNodeFontSize) so a bigger label always sits on a bigger dot. */\nexport function calculateNodeDotRadius(itemCount: number, range: ItemCountRange): number {\n const minArea = NODE_DOT_MIN_RADIUS ** 2;\n const maxArea = (range.maxDotRadius ?? NODE_DOT_MAX_RADIUS) ** 2;\n return Math.sqrt(minArea + logarithmicPosition(itemCount, range) * (maxArea - minArea));\n}\n\n// GenreTreeWheel tokens. Chips are spread evenly around the full circle (see getChipAngle in\n// wheel-geometry.ts). The wheel's reserved bottom strip needs to fit more than just the bare\n// circle (2 * WHEEL_RADIUS): a chip is centered *on* its point on that circle, so at the circle's\n// lowest point a chip's own half-height still sticks out past the diameter. Reserving that extra\n// half-height keeps every chip fully on screen, not just the ones nearest the top.\nexport const WHEEL_RADIUS = 260;\nexport const WHEEL_VIEWPORT_HEIGHT = WHEEL_RADIUS * 2 + MAX_NODE_HEIGHT / 2;\n\n// GenreTreeWheelRadialPopCore tokens. This renderer's circle must fit a full interactive pop\n// subtree inside it (not just chip clearance like the plain wheel), so it starts from a bigger\n// base floor than WHEEL_RADIUS even when no root has a pop side — leaving room to grow\n// further per calculatePopSubtreeRadialExtent (pop-core-radial-layout.ts) once one does.\nexport const WHEEL_POP_CORE_RADIUS = 945;\n\n// Radial (depth-axis) step for a pop subtree's polar layout — analogous to\n// VERTICAL_ORIENTATION_DEPTH_SEPARATION for the cartesian renderers, but expressed directly as a\n// radius increment per depth since pop-core-radial-layout.ts positions nodes in polar coordinates.\nexport const POP_TREE_DEPTH_RADIAL_SPACING = NODE_DIMENSIONS.WIDTH * 0.35;\n\n// Suggested size for the fixed-size ancestor any GenreTree/GenreTreeWheel variant requires (see\n// GenreTreeWheel's own doc comment): enough room for a typically-deep tree, or the wheel's chip\n// strip plus a typically-deep tree above it. Not derived from WHEEL_VIEWPORT_HEIGHT/WHEEL_RADIUS\n// — those size the wheel-of-chips only, not the variable-depth tree growing above it.\nexport const DEFAULT_FRAME_WIDTH = 1200;\nexport const DEFAULT_FRAME_HEIGHT = 750;\n\nexport const WHEEL_ROTATION_TRANSITION_MS = 500;\nexport const WHEEL_ROTATION_EASING = \"cubic-bezier(0.22, 1, 0.36, 1)\";\n\n// Zoom/pan tokens (see zoom-pan.ts). Bounds keep the tree from shrinking past legibility or\n// growing so large that a single wheel tick jumps an unreasonable amount.\nexport const ZOOM_MIN_SCALE = 0.05;\nexport const ZOOM_MAX_SCALE = 3;\n// Exponent multiplier applied to a trackpad ctrl+wheel/pinch event's deltaY (see\n// classifyWheelEvent in zoom-pan.ts — physical mouse wheel notches use ZOOM_BUTTON_SCALE_STEP\n// instead) — small because deltaY is typically tens to hundreds of pixels per tick, and exp()\n// amplifies fast. Raised repeatedly (0.0015 -> 0.003 -> 0.006 -> 0.012 -> 0.03): even with the\n// per-event animation delay removed, trackpad zoom still felt less sensitive than Google Maps'.\nexport const ZOOM_WHEEL_SCALE_SPEED = 0.03;\n// Caps the exponent (currentScale's multiplier is exp(this)) any single wheel/trackpad event can\n// apply, regardless of ZOOM_WHEEL_SCALE_SPEED. Researched d3-zoom's own wheel handling (the\n// library this project is built on) and Mapbox GL's scroll-zoom: both scale multiplicatively per\n// event exactly like computeZoomScale does, with no special curve to counter it — \"a faster swipe\n// zooms more\" is inherent to any proportional response, not a bug, since deltaY already grows with\n// gesture speed. What those implementations don't hit is an occasional very fast swipe producing a\n// runaway multiplicative jump in one single event; this cap bounds that per-event ratio to\n// exp(0.5) ≈ 1.65x without touching the feel of normal-speed gestures (which stay well under it).\nexport const ZOOM_WHEEL_MAX_EXPONENT = 0.5;\n// Multiplicative step applied per click of the zoom in/out buttons — a fallback control for\n// ctrl+scroll/pinch, which some trackpad/OS/browser combinations never translate into a\n// ctrlKey wheel event at all.\nexport const ZOOM_BUTTON_SCALE_STEP = 1.2;\n// Exponent applied to a touch pinch gesture's finger-distance ratio, amplifying the scale change\n// per unit of finger travel — touch pinch reaches JS as raw pointer events (see use-pan-zoom.ts),\n// with no OS-level amplification like trackpad pinch gets, so a 1:1 ratio feels sluggish. Lowered\n// from 1.5: on Android touchscreens that amplification made pinch-zoom feel too fast/twitchy.\nexport const ZOOM_PINCH_SCALE_SPEED = 1;\n// Duration (ms) of the eased glide between physical-mouse-wheel notches (see classifyWheelEvent\n// in zoom-pan.ts) — mirrors Google Maps' smooth zoom-and-settle for that chunky, low-frequency\n// input. Trackpad ctrl+wheel/pinch is applied instantly instead (see handleWheel in\n// use-pan-zoom.ts): it's already a continuous, high-frequency stream, so easing each event would\n// only stack added latency behind it rather than smooth anything.\nexport const ZOOM_ANIMATION_DURATION_MS = 220;\n// Breathing room (px) kept around content when \"fit to frame\" computes a scale — content is\n// never scaled to touch the viewport's edges exactly.\nexport const ZOOM_FIT_PADDING = 40;\n// Minimum scale a clicked node is zoomed/centered to (see usePanZoom's centerOnElement) — a\n// comfortable reading scale: zooms in when the user is farther out, but never zooms out when\n// they're already closer.\n// 0.2 (not 1): full scale read as \"too zoomed in\" once clicked, cropping most of the surrounding tree.\nexport const ZOOM_FOCUS_SCALE = 0.2;\n// On-screen font size (px) of radial dot labels, smallest to biggest genre: labels keep this size\n// at every zoom like place names on a map, so zooming out hides small labels (bigger in layout\n// units, they collide more) and zooming in brings them back.\nexport const LABEL_SCREEN_MIN_PX = 11;\nexport const LABEL_SCREEN_MAX_PX = 22;\n// Minimum log-scaled itemCount position (0 = smallest genre, 1 = biggest) a label needs to be shown\n// at ZOOM_MIN_SCALE, easing to 0 at ZOOM_FOCUS_SCALE: like a map, a far-out view names only the\n// main genres even where smaller labels would fit.\nexport const LABEL_MIN_RANK_AT_MIN_ZOOM = 0.6;\n// Rank (see calculateLabelRank) a label loses per hierarchy level below its root's direct children.\nexport const LABEL_DEPTH_RANK_PENALTY = 0.15;\nexport const ROOT_LABEL_MIN_SCREEN_PX = 15;\n// Quiet time (ms) after the last zoom change before dot labels are re-placed for the new scale.\nexport const LABEL_RELAYOUT_DEBOUNCE_MS = 120;\n// On-screen font size (px) of the zoomed-in root's region label: placed at TARGET, re-placed once\n// zooming drifts it outside [MIN, MAX] (see WheelRadialPopCoreCore).\nexport const REGION_LABEL_TARGET_PX = 28;\nexport const REGION_LABEL_MIN_PX = 16;\nexport const REGION_LABEL_MAX_PX = 48;\n// Duration (ms) of the eased pan+zoom glide centerOnElement flies to a clicked node with — longer\n// than ZOOM_ANIMATION_DURATION_MS's wheel-notch glide since this one also covers a pan distance,\n// not just a scale change, and should read as one deliberate \"fly to\" (Google Maps' own\n// click-to-center glide) rather than an instant jump.\nexport const CENTER_ON_ELEMENT_DURATION_MS = 450;\n// How long a single click waits for a second one before acting (see defer-click.ts).\nexport const DOUBLE_CLICK_DELAY_MS = 250;\n// Minimum content (px) usePanZoom's pan clamp always keeps on-screen along each axis — dragging\n// or wheel-panning can bring the tree to this close to sliding fully out of the viewport, but no\n// closer, so the user can never lose it entirely and have no visible edge to drag back from.\nexport const PAN_MIN_VISIBLE_PX = 80;\n\n// Idle time after the last wheel/drag/pinch event before usePanZoom commits its imperatively\n// tracked pan/zoom into React state (re-rendering and re-running the zoom-dependent label effects).\nexport const PAN_ZOOM_SETTLE_MS = 100;\n\n// After usePanZoom applies a `defaultViewport`, centerOnElement is ignored for this long so the\n// initial `selectedNodeId` (which centers on mount) doesn't glide away from the restored view.\nexport const DEFAULT_VIEWPORT_CENTER_GUARD_MS = 1000;\n\n// Fixed width (px) of the node info panel (see InfoPanel.tsx) — also the collision threshold\n// resolveInfoPanelSide compares a clicked node's screen rect against, so the panel never renders\n// directly on top of the node that opened it.\nexport const INFO_PANEL_WIDTH = 280;\n// At or below this container width (px) the info panel docks to the bottom as a sheet — mirrors\n// the `@container (max-width: 767px)` rule in styles.css.\nexport const INFO_PANEL_SHEET_MAX_WIDTH = 767;\n// Share of the container's height the sheet covers at its peek snap — mirrors the sheet's\n// `height: 40%` in styles.css.\nexport const INFO_PANEL_SHEET_PEEK_FRACTION = 0.4;\n// Vertical drag (px) on the sheet's grabber past which it snaps instead of treating it as a tap.\nexport const INFO_PANEL_SHEET_DRAG_THRESHOLD_PX = 32;\n","import type { ReactNode } from \"react\";\n\nexport interface GenreTreeNode {\n id: string;\n parentId: string | null;\n name: string;\n /** Subtree total: this node's own items plus all its descendants'. Parents must already include\n * their children's counts — the library displays and sizes by this value as-is, never summing. */\n itemCount: number;\n /** Whether add-child, rename, delete and reparent actions apply to this node. Defaults to true. */\n actionable?: boolean;\n /** Meaningful only for a root's direct children: \"core\" (default when unset) marks the\n * required non-pop branch, \"pop\" marks the optional pop/crossover branch. Ignored everywhere\n * else. Only interpreted by GenreTreeWheelRadialPopCore. */\n side?: \"core\" | \"pop\";\n /** A root kept apart from the tree (e.g. an uncategorized bucket). Only interpreted by\n * GenreTreeOutline, which lists it in its own section after the tree. */\n detached?: boolean;\n /** Stable genre key (a wikidata QID or a synthetic \"LOCAL:\" key). Read on root nodes only, to\n * pick the root's fixed color (see getGenreTreeColor). */\n wikidataId?: string | null;\n /** A grouping node that no item is filed under directly. Read by InfoPanel (badge), GenreTreeOutline\n * and the stacked/wheel node labels (italic); the pop-core labels ignore it. */\n umbrella?: boolean;\n /** Which scene a genre belongs to. GenreTreeWheelRadialPopCore draws an \"underground\" node and its\n * descendants in a darker shade of their usual color, over a darker background slice. */\n scene?: \"commercial\" | \"underground\";\n}\n\nexport type GenreTreePlayState = \"playing\" | \"paused\" | \"loading\";\n\n/** \"stacked\" is the linear/card tree (GenreTree); \"wheel\"/\"pop-core\" are the radial layouts\n * (GenreTreeWheel* family); \"outline\" is the nested-list text view (GenreTreeOutline). Shared by\n * GenreTreeViewSkeleton so a consumer's loading state can match whichever real layout it's about\n * to render. */\nexport type GenreTreeViewMode = \"stacked\" | \"wheel\" | \"pop-core\" | \"outline\";\n\n/** \"horizontal\" grows children rightward from a left root (default). \"vertical\" grows children\n * upward from a bottom-anchored root — used by GenreTreeWheel. \"horizontal-anchored\" grows\n * rightward like \"horizontal\" but anchors the root at a fixed local coordinate\n * (0, svgHeight / 2) regardless of tree shape, instead of \"horizontal\"'s unanchored top-aligned\n * bounding box — used by GenreTreeWheelRight. \"vertical-flipped\" mirrors \"vertical\": children\n * grow downward from a top-anchored root. \"horizontal-anchored-flipped\" mirrors\n * \"horizontal-anchored\": children grow leftward from a root anchored at (svgWidth, svgHeight / 2).\n * The four anchored/mirrored variants are used by GenreTreeWheelRadial's top/right/bottom/left\n * cardinals respectively (vertical / horizontal-anchored / vertical-flipped /\n * horizontal-anchored-flipped). */\nexport type TreeOrientation =\n | \"horizontal\"\n | \"vertical\"\n | \"horizontal-anchored\"\n | \"vertical-flipped\"\n | \"horizontal-anchored-flipped\";\n\n/** True for the two orientations whose depth axis is Y (root-to-leaf grows vertically) rather\n * than X. */\nexport function isVerticalOrientation(orientation: TreeOrientation): boolean {\n return orientation === \"vertical\" || orientation === \"vertical-flipped\";\n}\n\n/** +1 if depth increases in the positive direction of its axis (down for \"vertical-flipped\",\n * right for \"horizontal\"/\"horizontal-anchored\"); -1 if it increases in the negative direction (up\n * for \"vertical\", left for \"horizontal-anchored-flipped\"). Determines which side of a card is\n * \"toward the leaves\" so the hover toolbar/hit-rect always extends that way, never toward the\n * parent. */\nexport function depthAxisSign(orientation: TreeOrientation): 1 | -1 {\n if (orientation === \"vertical\" || orientation === \"horizontal-anchored-flipped\") return -1;\n return 1;\n}\n\nexport interface GenreTreeAction {\n key: string;\n icon: (node: GenreTreeNode) => ReactNode;\n label: (node: GenreTreeNode) => string;\n onClick: (event: React.MouseEvent, node: GenreTreeNode) => void;\n enabled?: (node: GenreTreeNode) => boolean;\n danger?: boolean;\n /** \"primary\" = inline icon-row action (old upload slot); \"overflow\" = kebab-menu action\n * (old rename/delete slot). Defaults to \"overflow\". */\n placement?: \"primary\" | \"overflow\";\n}\n\n/** How the mouse wheel drives an interactive renderer's pan/zoom viewport — see\n * `GenreTreeProps.wheelZoom`. */\nexport type WheelZoomMode = \"always\" | \"modifier\";\n\n/** Layout of the floating zoom/pop/fit controls — see `GenreTreeProps.controlsOrientation`. */\nexport type ControlsOrientation = \"horizontal\" | \"vertical\";\n\n/** When the floating zoom in / zoom out / fit buttons show — see `GenreTreeProps.showZoomControls`. */\nexport type ZoomControlsVisibility = \"auto\" | \"always\" | \"never\";\n\n/** The pan/zoom view as the content-space point shown at the viewport's center plus the scale —\n * independent of the container's pixel size, so a saved view lands on the same spot in any window. */\nexport interface GenreTreeViewport {\n x: number;\n y: number;\n zoom: number;\n}\n\n/** The info panel's built-in sections, handed to `renderInfoPanelBody` for placement. Each is\n * `null` when it has nothing to show. */\nexport interface InfoPanelSections {\n /** The node's action toolbar — null when `showToolbar` is off or a reparent is in progress. */\n toolbar: ReactNode;\n /** Ancestor chips, root-first — null for a root. */\n ancestors: ReactNode;\n /** Parent, node and children mini tree. */\n family: ReactNode;\n /** The `itemCountLabel` row. */\n itemCount: ReactNode;\n}\n\n/** The info panel body a `renderInfoPanelBody` returns. */\nexport interface InfoPanelBody {\n /** The wide column when expanded; on top when compact. */\n primary: ReactNode;\n /** The narrow left column when expanded; below `primary` when compact. Omit (or null) for a\n * single column. */\n aside?: ReactNode;\n}\n\nexport interface GenreTreeProps {\n nodes: GenreTreeNode[];\n /** Label for the info panel's `itemCount` row (e.g. \"Song count\"); the library can't know what unit is counted. */\n itemCountLabel: string;\n /** Optional total for the current itemCount metric. When provided, node sizes use a fixed\n * logarithmic domain from zero to this total, so filtering recalculates each node's size against\n * the filtered total. The total must use the same metric as GenreTreeNode.itemCount. */\n itemCountScaleMax?: number;\n /** Full node count used to adapt maximum node/card/dot size to visualization density. Defaults\n * to nodes.length; wheel renderers pass their complete dataset to their selected subtree. */\n nodeCountForSizing?: number;\n className?: string;\n rootColor?: string;\n orientation?: TreeOrientation;\n /** Omits the root node's own card (and its toolbar) from rendering, keeping only its\n * connecting paths to its children — used when the root is represented elsewhere, e.g.\n * GenreTreeWheel's chip. Defaults to false. */\n hideRoot?: boolean;\n playingNodeId?: string | null;\n playState?: GenreTreePlayState;\n /** Id of the node currently being reassigned to a new parent (owned by the consumer, since it can span multiple GenreTree instances). */\n reparentingNodeId?: string | null;\n onPlayPause?: (nodeId: string) => void;\n onAddChild?: (parentId: string) => void;\n onRenameRequest?: (node: GenreTreeNode) => void;\n onDeleteRequest?: (node: GenreTreeNode) => void;\n /** Fired when the user clicks \"Change parent\" on a node — the consumer should set `reparentingNodeId` to this node's id. */\n onReparentRequest?: (node: GenreTreeNode) => void;\n /** Fired when the user picks a target node while `reparentingNodeId` is set. */\n onReparent?: (nodeId: string, newParentId: string) => void | Promise<void>;\n /** Fired when the user clicks a node's body (its card, or its chip in the wheel renderers) —\n * anywhere but a toolbar/menu action or, while `reparentingNodeId` is set, the reparent-target\n * overlay. The library itself opens its own read-only info panel for the clicked node\n * alongside this callback; the consumer owns any additional selection state or highlighting. */\n onNodeClick?: (node: GenreTreeNode, event: MouseEvent) => void;\n /** Fired when the pointer enters a node (or, in `GenreTreeOutline`, its name receives focus) —\n * e.g. to prefetch whatever the consumer loads on click. Fired by `GenreTreeOutline` and\n * `GenreTreeWheelRadialPopCore`; accepted and ignored by the other renderers. */\n onNodeHover?: (node: GenreTreeNode) => void;\n /** Extra actions rendered alongside the built-in play/add-child/rename/delete/reparent set —\n * placement \"primary\" renders inline on the node (the upload slot from earlier versions used),\n * \"overflow\" (the default) renders in the kebab menu alongside rename/delete. */\n additionalActions?: (node: GenreTreeNode) => GenreTreeAction[];\n /** When true (the default), GenreTree owns its own pan/zoom viewport, zoom buttons, and\n * wheel/drag listeners. Set to false to render just the bare SVG at its natural size with none\n * of that — used by GenreTreeWheel, which applies one shared pan/zoom transform to the tree and\n * its wheel together instead of giving the tree its own independent one. */\n interactive?: boolean;\n /** \"always\" (the default): the viewport captures every wheel event — any wheel/pinch zooms at\n * the cursor, drag pans (Google Maps style) — and all touch input. \"modifier\": only Ctrl/Meta+wheel (or a\n * trackpad pinch) zooms; a plain wheel and one-finger vertical touch scroll the page instead,\n * for trees embedded in a scrolling list. Ignored when `interactive` is false. */\n wheelZoom?: WheelZoomMode;\n /** Stacks the floating zoom/pop/fit controls vertically (\"vertical\", the default) or lays them\n * out in a row (\"horizontal\"). */\n controlsOrientation?: ControlsOrientation;\n /** \"auto\" (the default) hides the zoom in / zoom out / fit buttons on touch screens\n * (`pointer: coarse`), where pinch and pan replace them; \"always\" and \"never\" force them. The\n * `floatingControlsLeading` content stays visible either way. */\n showZoomControls?: ZoomControlsVisibility;\n /** Host content rendered in the floating controls box, beside the zoom buttons (\"horizontal\")\n * or above them (\"vertical\"); stacked above them in narrow containers. */\n floatingControlsLeading?: ReactNode;\n /** Shows a small spinner beside the zoom buttons (after a short delay, so fast operations don't\n * flash) and marks the tree `aria-busy`. Use it while the host is updating the tree in place. */\n busy?: boolean;\n /** Multiplies the spacing between depth levels along the tree's growth axis. Defaults to 1\n * (no change). */\n depthSpacingScale?: number;\n /** When false, the info panel omits the node actions (play/pause, add-child, rename, reparent,\n * delete, extra actions). Defaults to true. */\n showToolbar?: boolean;\n /** Overrides which node is shown highlighted (with unrelated nodes/links dimmed) instead of\n * GenreTree's own internally-tracked clicked node — used by `interactive={false}` instances\n * (e.g. GenreTreeWheel's nested tree) whose own info panel, and thus own selection, is owned\n * by an ancestor rather than by this instance. On the top-level exported renderers\n * (`GenreTreeWheel`, `GenreTreeWheelRight`, `GenreTreeWheelRadial`,\n * `GenreTreeWheelRadialPopCore`, `GenreTree` itself), setting this also opens and centers the\n * info panel for that node — the same effect a direct click on it would have — swapping in its\n * owning root/sector first if needed. Leave unset to use the internal selection. */\n selectedNodeId?: string | null;\n /** Composes the info panel body for the clicked node: places the built-in `sections` (any\n * omitted one is not rendered) among the consumer's own content — e.g. a consumer-fetched detail\n * like essential tracks. The library has no knowledge of what this renders (loading state, data\n * fetching, etc. are entirely the consumer's responsibility, mirroring `additionalActions`).\n * Omitted, the body is the built-in sections in one column. The panel frame (header, columns,\n * scrolling, edit-mode danger zone) stays the library's. */\n renderInfoPanelBody?: (node: GenreTreeNode, sections: InfoPanelSections) => InfoPanelBody;\n /** Fired when a user action changes the node shown in the info panel (a click on the tree, or on\n * a parent/child/ancestor chip in the panel), or with `null` when the user closes it (its header\n * ✕) — so a consumer driving `selectedNodeId` stays in sync. Not fired when re-selecting the node\n * already shown, when the panel follows the consumer's own `selectedNodeId`, nor when the\n * consumer clears `selectedNodeId` to `null`/`undefined` (which closes the panel). */\n onSelectedNodeChange?: (node: GenreTreeNode | null) => void;\n /** Hides the info panel header's close button — for a consumer that closes the panel itself\n * (by clearing `selectedNodeId`), e.g. from its own search bar. Defaults to false. */\n hideInfoPanelClose?: boolean;\n /** Hides the info panel header's node name — for a consumer that already shows it, e.g. in its own\n * search bar floated over the header (see `--gtv-info-panel-header-inset`). Defaults to false. */\n hideInfoPanelTitle?: boolean;\n /** Keeps `renderDangerZone` inactive. Defaults to false. */\n readOnly?: boolean;\n /** Rendered at the bottom of the info panel unless `readOnly`. */\n renderDangerZone?: (node: GenreTreeNode) => ReactNode;\n /** Whether the info panel fills the tree container (controlled), splitting into a narrow left\n * column (the body's `aside`, then the danger zone) and a wide right one (its `primary`). Selecting\n * another node from the panel keeps it expanded. Defaults to false. */\n expanded?: boolean;\n /** When set, the info panel header shows an expand/collapse toggle firing this with the\n * requested state, and Escape fires it with `false` while expanded (unless focus is in an\n * input/textarea/select or a dialog is open). */\n onExpandedChange?: (on: boolean) => void;\n /** Slides the info panel out of view and makes it inert, keeping it mounted. Defaults to false. */\n collapsed?: boolean;\n /** Drops the header's expand/collapse toggle when the host draws its own control for it. */\n hideExpandButton?: boolean;\n /** Consumer content shown over the tree independent of node selection — at the expanded info\n * panel's size (e.g. an About page), or as an expandable info-panel peer when it sets\n * `onExpandedChange` (e.g. an artist). Its header ✕ and Escape (same exceptions as\n * `onExpandedChange`) fire `onClose`; Escape collapses an expanded panel first. Showing it\n * alongside the info panel is the consumer's call — close one before opening the other to keep\n * them exclusive. */\n overlayPanel?: GenreTreeOverlayPanel;\n /** Seeds the pan/zoom on mount, replacing the initial fit-to-frame and the centering on an\n * initial `selectedNodeId`. Read once; later changes are ignored. Ignored when `interactive` is\n * false and by `GenreTreeOutline`. */\n defaultViewport?: GenreTreeViewport;\n /** Fired once a user-driven pan/zoom settles (drag, wheel, pinch, zoom/fit buttons, centering on\n * a clicked node) — not for the initial fit or for applying `defaultViewport`. */\n onViewportChange?: (viewport: GenreTreeViewport) => void;\n}\n\nexport interface GenreTreeOverlayPanel {\n title: string;\n /** Its top-level children (and `extraContent`'s) are separated by a divider, as is `extraContent`\n * from `content` when stacked — blocks must not draw their own. */\n content: ReactNode;\n onClose: () => void;\n /** With `onExpandedChange`, the panel behaves like the info panel: compact in its left slot,\n * expanding to its expanded size (controlled). Without it, the panel always covers the center\n * column at the expanded size, with no toggle. */\n expanded?: boolean;\n /** Adds the header's expand/collapse toggle; Escape fires it with `false` while expanded. */\n onExpandedChange?: (on: boolean) => void;\n /** Stacked below `content` when compact, the right column when expanded — same as the info\n * panel body's `primary`. */\n extraContent?: ReactNode;\n /** See GenreTreeProps.collapsed. */\n collapsed?: boolean;\n /** See GenreTreeProps.hideExpandButton. */\n hideExpandButton?: boolean;\n /** See GenreTreeProps.hideInfoPanelTitle. */\n hideTitle?: boolean;\n /** See GenreTreeProps.hideInfoPanelClose; Escape still closes. */\n hideClose?: boolean;\n}\n","import * as d3 from \"d3\";\n\nexport function addGrid(\n svg: d3.Selection<SVGGElement, unknown, null, undefined>,\n width: number,\n height: number,\n gridIsHidden: boolean,\n): void {\n const SPACING = 50;\n if (!gridIsHidden) {\n const gridGroup = svg.append(\"g\").attr(\"class\", \"grid\");\n\n for (let y = 0; y <= height; y += SPACING) {\n gridGroup\n .append(\"line\")\n .attr(\"x1\", 0)\n .attr(\"y1\", y)\n .attr(\"x2\", width)\n .attr(\"y2\", y)\n .attr(\"stroke\", \"#ccc\")\n .attr(\"stroke-width\", 1);\n\n gridGroup.append(\"text\").attr(\"x\", 5).attr(\"y\", y - 5).attr(\"fill\", \"#000\").attr(\"font-size\", \"10px\").text(y);\n }\n\n for (let x = 0; x <= width; x += SPACING) {\n gridGroup\n .append(\"line\")\n .attr(\"x1\", x)\n .attr(\"y1\", 0)\n .attr(\"x2\", x)\n .attr(\"y2\", height)\n .attr(\"stroke\", \"#ccc\")\n .attr(\"stroke-width\", 1);\n\n gridGroup.append(\"text\").attr(\"x\", x + 5).attr(\"y\", 15).attr(\"fill\", \"#000\").attr(\"font-size\", \"10px\").text(x);\n }\n }\n}\n","import * as d3 from \"d3\";\n\nimport {\n GenreTreeNode,\n isVerticalOrientation,\n TreeOrientation,\n} from \"../types\";\nimport {\n CONNECTOR_COLOR,\n CONNECTOR_OPACITY,\n calculateLinkWidth,\n calculateNodeDimensions,\n ItemCountRange,\n} from \"../constants\";\n\ntype D3Selection = d3.Selection<SVGGElement, unknown, null, undefined>;\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\ntype D3Link = d3.HierarchyLink<GenreTreeNode>;\n\nexport interface RoundedRectCorners {\n tl: number;\n tr: number;\n br: number;\n bl: number;\n}\n\n// SVG <rect> only takes one uniform rx/ry pair, so a node card needs a <path> instead to square\n// off just its top corners while its hover tab is attached (see tree-renderer.ts's mouseover/\n// mouseleave-timeout handlers), and stay fully rounded otherwise.\nexport function roundedRectPath(\n x: number,\n y: number,\n width: number,\n height: number,\n corners: RoundedRectCorners,\n) {\n const { tl, tr, br, bl } = corners;\n return [\n `M ${x + tl} ${y}`,\n `H ${x + width - tr}`,\n tr ? `A ${tr} ${tr} 0 0 1 ${x + width} ${y + tr}` : \"\",\n `V ${y + height - br}`,\n br ? `A ${br} ${br} 0 0 1 ${x + width - br} ${y + height}` : \"\",\n `H ${x + bl}`,\n bl ? `A ${bl} ${bl} 0 0 1 ${x} ${y + height - bl}` : \"\",\n `V ${y + tl}`,\n tl ? `A ${tl} ${tl} 0 0 1 ${x + tl} ${y}` : \"\",\n \"Z\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\n// Traces only the right, bottom, and left edges of a rect (skipping the top edge entirely,\n// unclosed) so a node card's border can merge with its hover tab above it instead of drawing a\n// visible line across the seam. Assumes the top corners are already square (radius 0) — see the\n// mouseover/mouseleave-timeout handlers in tree-renderer.ts, which only swap to this path once\n// the fill path's top corners are squared too.\nexport function openBottomBorderPath(\n x: number,\n y: number,\n width: number,\n height: number,\n corners: Pick<RoundedRectCorners, \"br\" | \"bl\">,\n) {\n const { br, bl } = corners;\n return [\n `M ${x + width} ${y}`,\n `V ${y + height - br}`,\n br ? `A ${br} ${br} 0 0 1 ${x + width - br} ${y + height}` : \"\",\n `H ${x + bl}`,\n bl ? `A ${bl} ${bl} 0 0 1 ${x} ${y + height - bl}` : \"\",\n `V ${y}`,\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\nexport function appendPaths(\n d3Lib: typeof import(\"d3\"),\n svg: D3Selection,\n treeData: D3Node,\n itemCountRange: ItemCountRange,\n orientation: TreeOrientation = \"horizontal\",\n selectedNodeId: string | null = null,\n) {\n const x = (d: D3Node) =>\n d.x! + calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH / 2;\n const y = (d: D3Node) =>\n d.y! + calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT / 2;\n\n const linkGenerator = isVerticalOrientation(orientation)\n ? d3Lib.linkVertical<D3Link, D3Node>().x(x).y(y)\n : d3Lib.linkHorizontal<D3Link, D3Node>().x(x).y(y);\n\n // A link is \"related\" to the selected node when the selected node is one of its two endpoints\n // (its parent link, or a link to one of its children) — every other link in the tree dims.\n const isRelatedLink = (d: D3Link) =>\n d.source.data.id === selectedNodeId || d.target.data.id === selectedNodeId;\n\n svg\n .selectAll(\"path.gtv-link\")\n .data(treeData.links())\n .enter()\n .append(\"path\")\n .attr(\n \"class\",\n (d) =>\n \"gtv-link\" +\n (selectedNodeId && !isRelatedLink(d) ? \" gtv-link--dimmed\" : \"\"),\n )\n .attr(\"d\", linkGenerator)\n .style(\"fill\", \"none\")\n .style(\"stroke\", CONNECTOR_COLOR)\n .style(\"stroke-width\", (d) =>\n calculateLinkWidth(d.target.data.itemCount, itemCountRange),\n )\n .style(\"stroke-opacity\", CONNECTOR_OPACITY)\n .style(\"stroke-linecap\", \"round\");\n}\n","import * as d3 from \"d3\";\n\nimport {\n depthAxisSign,\n GenreTreeAction,\n GenreTreeNode,\n GenreTreePlayState,\n isVerticalOrientation,\n TreeOrientation,\n} from \"./types\";\nimport {\n HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES,\n VERTICAL_SEPARATION_BETWEEN_RECTANGLES,\n DEPTH_AXIS_SPACING_FACTOR,\n ACTIONS_OVERLAY_WIDTH,\n ACTIONS_OVERLAY_HEIGHT,\n SURFACE_BORDER_COLOR,\n CORNER_RADIUS,\n ELEVATION,\n TEXT_COLOR,\n TEXT_MUTED_COLOR,\n ACCENT_COLOR,\n ACCENT_TEXT_COLOR,\n MAX_NODE_WIDTH,\n MAX_NODE_HEIGHT,\n calculateNodeBorderWidth,\n calculateNodeDimensions,\n calculateNodeFontSize,\n getItemCountRange,\n ItemCountRange,\n tintSurface,\n} from \"./constants\";\nimport { addGrid } from \"./d3-helper/d3-grid-helper\";\nimport {\n appendPaths,\n roundedRectPath,\n} from \"./d3-helper/d3-path-helper\";\nimport {\n addReparentTargetOverlay,\n} from \"./NodeHelper\";\n\ntype D3Selection = d3.Selection<SVGGElement, unknown, null, undefined>;\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\n\ninterface SvgDimensions {\n svgWidth: number;\n svgHeight: number;\n highestVerticalCoordinate: number;\n rootDepthOffset: number;\n}\n\nexport interface RenderTreeCallbacks {\n onPlayPause?: (nodeId: string) => void;\n onAddChild?: (parentId: string) => void;\n onRenameRequest?: (node: GenreTreeNode) => void;\n onDeleteRequest?: (node: GenreTreeNode) => void;\n onReparentRequest?: (node: GenreTreeNode) => void;\n onReparentTargetSelect: (newParentId: string) => void;\n onNodeClick?: (node: GenreTreeNode, event: MouseEvent) => void;\n additionalActions?: (node: GenreTreeNode) => GenreTreeAction[];\n playingNodeId?: string | null;\n playState?: GenreTreePlayState;\n}\n\nfunction getLayoutSpacings(itemCountRange: ItemCountRange, depthSpacingScale = 1) {\n const maxWidth = itemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH;\n const maxHeight = itemCountRange.maxNodeHeight ?? MAX_NODE_HEIGHT;\n const verticalSeparation = maxHeight + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;\n return {\n horizontalDepth:\n (maxWidth + ACTIONS_OVERLAY_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES) *\n DEPTH_AXIS_SPACING_FACTOR *\n depthSpacingScale,\n verticalBreadth: verticalSeparation,\n verticalDepth: verticalSeparation * 2 * DEPTH_AXIS_SPACING_FACTOR * depthSpacingScale,\n verticalSibling: maxWidth + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES,\n };\n}\n\nexport function calculateSvgDimensions(\n d3Lib: typeof import(\"d3\"),\n treeData: D3Node,\n orientation: TreeOrientation = \"horizontal\",\n hideRoot = false,\n depthSpacingScale = 1,\n suppliedItemCountRange?: ItemCountRange,\n): SvgDimensions {\n const treeNodes = treeData.descendants();\n const nodes = treeNodes;\n const itemCountRange = suppliedItemCountRange ?? getItemCountRange(\n treeNodes.map((d) => d.data),\n );\n const maxNodeDimensions = {\n WIDTH: itemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH,\n HEIGHT: itemCountRange.maxNodeHeight ?? MAX_NODE_HEIGHT,\n };\n const maximumLevel = d3Lib.max(nodes, (d) => d.depth)!;\n const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);\n const depthSeparation = layoutSpacings.verticalDepth;\n const depthSeparationHorizontal = layoutSpacings.horizontalDepth;\n\n if (isVerticalOrientation(orientation)) {\n // Root sits at the bottom edge (\"vertical\") or top edge (\"vertical-flipped\"), so only that\n // one side needs toolbar/menu clearance — the\n // horizontal branch below reserves the same ACTIONS_OVERLAY_HEIGHT split in half across its\n // two ends (top and bottom margins), so match that per-side amount rather than the full budget.\n // A hidden root renders no card and no toolbar, so it needs neither its own height nor that\n // clearance — the svg's bottom edge instead lands exactly on the root's anchor point.\n const svgHeight = hideRoot\n ? maximumLevel * depthSeparation\n : maximumLevel * depthSeparation +\n maxNodeDimensions.HEIGHT +\n ACTIONS_OVERLAY_HEIGHT / 2;\n\n // Root is centered over its children (Reingold–Tilford), so anchoring the whole svg on the\n // root's own breadth coordinate — rather than the bounding box's midpoint — keeps the root\n // exactly at svgWidth/2. The toolbar only ever renders to a node's right, so only the right\n // half needs its headroom (ACTIONS_OVERLAY_WIDTH); the left half doesn't.\n //\n // Every consumer of a node's x (link endpoints in appendPaths, the rendered card's own\n // center) treats `d.x + own width / 2` as that node's true visual position — d.x alone is\n // just its left edge. The root is no exception (hideRoot only skips drawing its card, not\n // this convention), so it has to be centered on that same point, not on the bare `d.x`.\n // Skipping the `+ width / 2` here left the root's card/chip sitting half its own width left\n // of where its children's links actually converge.\n const rootWidth = calculateNodeDimensions(\n treeData.data.itemCount,\n itemCountRange,\n ).WIDTH;\n const rootAnchorX = treeData.x! + rootWidth / 2;\n\n const leftmostBreadthCoordinate = d3Lib.min(nodes, (d) => d.x)!;\n const rightmostBreadthCoordinate = d3Lib.max(nodes, (d) => d.x)!;\n const leftHalfExtent =\n rootAnchorX - leftmostBreadthCoordinate + maxNodeDimensions.WIDTH / 2;\n const rightHalfExtent =\n rightmostBreadthCoordinate -\n rootAnchorX +\n maxNodeDimensions.WIDTH / 2 +\n ACTIONS_OVERLAY_WIDTH;\n const svgWidth = Math.max(leftHalfExtent, rightHalfExtent) * 2;\n\n // Reuses this slot to carry the root-centering offset applied in setupTreeLayout, rather\n // than a vertical coordinate — the two orientations need different single scalars out of\n // this function and the field isn't worth renaming just for that.\n const rootCenteringOffset = svgWidth / 2 - rootAnchorX;\n\n // \"vertical\": root sits at the depth axis's far end (y = svgHeight); the universal\n // `+ height / 2` rendering convention (see appendPaths/renderTree) then pulls a hidden root's\n // link-anchor point back toward y = svgHeight, i.e. toward the anchor it needs to coincide\n // with — no extra offset needed, same as \"horizontal-anchored-flipped\" below.\n // \"vertical-flipped\": root sits at the depth axis's near end (y = 0) instead, so that same\n // `+ height / 2` convention pushes a hidden root's anchor point *away* from y = 0 by exactly\n // one half-height — the mirror of \"horizontal-anchored\" needing `-rootWidth`, this needs an\n // analogous `-rootHeight` pulled back into the depth axis so the push lands exactly at y = 0.\n const rootHeight = calculateNodeDimensions(\n treeData.data.itemCount,\n itemCountRange,\n ).HEIGHT;\n const rootDepthOffset = hideRoot ? -rootHeight : 0;\n\n return {\n svgWidth,\n svgHeight,\n highestVerticalCoordinate: rootCenteringOffset,\n rootDepthOffset,\n };\n }\n\n if (\n orientation === \"horizontal-anchored\" ||\n orientation === \"horizontal-anchored-flipped\"\n ) {\n const svgWidth =\n maximumLevel * depthSeparationHorizontal +\n maxNodeDimensions.WIDTH +\n ACTIONS_OVERLAY_WIDTH;\n\n // Root is centered over its children (Reingold–Tilford), so anchoring the whole svg on the\n // root's own breadth coordinate keeps the root exactly at svgHeight/2 — same technique as the\n // vertical branch above, but on the Y (breadth) axis since depth grows along X here instead.\n const rootHeight = calculateNodeDimensions(\n treeData.data.itemCount,\n itemCountRange,\n ).HEIGHT;\n const rootAnchorY = treeData.x! + rootHeight / 2;\n\n const topmostBreadthCoordinate = d3Lib.min(nodes, (d) => d.x)!;\n const bottommostBreadthCoordinate = d3Lib.max(nodes, (d) => d.x)!;\n const topHalfExtent =\n rootAnchorY -\n topmostBreadthCoordinate +\n maxNodeDimensions.HEIGHT / 2 +\n ACTIONS_OVERLAY_HEIGHT / 2;\n const bottomHalfExtent =\n bottommostBreadthCoordinate -\n rootAnchorY +\n maxNodeDimensions.HEIGHT / 2 +\n ACTIONS_OVERLAY_HEIGHT / 2;\n const svgHeight = Math.max(topHalfExtent, bottomHalfExtent) * 2;\n\n // Reuses this slot to carry the root-centering offset applied in setupTreeLayout, same as\n // the vertical branch above.\n const rootCenteringOffset = svgHeight / 2 - rootAnchorY;\n\n // A hidden root's own width is otherwise baked into every consumer's `d.x + width / 2`\n // convention (see the comment on the vertical branch above) same as it is for a hidden root's\n // breadth position — except here that convention pushes the root's rendered center *away*\n // from the anchor (depth grows in +x, toward larger x) instead of into it, since the anchor\n // sits at this axis's near/zero end rather than its far end the way the vertical branch's\n // bottom-pinned anchor does. Pulling everything back by the root's own width before that\n // `+ width / 2` is applied lands the root's rendered center exactly one half-width *behind*\n // x=0 — i.e. inside the anchor's chip, matching the vertical branch's root landing inside its\n // chip too. setupTreeLayout applies this shift to every node, not just the root: shifting only\n // the root would leave the root→depth-1 link one rootWidth longer than every later\n // depth-to-depth link, since a per-depth offset cancels out of a distance calculation only\n // when it's the same on both ends.\n const rootWidth = calculateNodeDimensions(\n treeData.data.itemCount,\n itemCountRange,\n ).WIDTH;\n const rootDepthOffset = hideRoot ? -rootWidth : 0;\n\n return {\n svgWidth,\n svgHeight,\n highestVerticalCoordinate: rootCenteringOffset,\n rootDepthOffset,\n };\n }\n\n const highestNodeVerticalCoordinate = d3Lib.min(nodes, (d) => d.x)!;\n const highestVerticalCoordinate =\n highestNodeVerticalCoordinate +\n maxNodeDimensions.HEIGHT / 2 -\n ACTIONS_OVERLAY_HEIGHT / 2;\n const lowestNodeVerticalCoordinate = d3Lib.max(nodes, (d) => d.x)!;\n const lowestVerticalCoordinate =\n lowestNodeVerticalCoordinate +\n maxNodeDimensions.HEIGHT / 2 +\n ACTIONS_OVERLAY_HEIGHT / 2;\n // When every node shares the same breadth coordinate (a single root, or a childless/straight\n // chain), the spread above collapses to just ACTIONS_OVERLAY_HEIGHT, dropping the node's own\n // height from the sum entirely. Floor at a single node's full footprint so the card always fits.\n const svgHeight = Math.max(\n lowestVerticalCoordinate - highestVerticalCoordinate,\n maxNodeDimensions.HEIGHT + ACTIONS_OVERLAY_HEIGHT,\n );\n\n const svgWidth =\n maximumLevel * depthSeparationHorizontal +\n maxNodeDimensions.WIDTH +\n ACTIONS_OVERLAY_WIDTH;\n\n return { svgWidth, svgHeight, highestVerticalCoordinate, rootDepthOffset: 0 };\n}\n\nexport function setupTreeLayout(\n d3Lib: typeof import(\"d3\"),\n treeData: D3Node,\n highestVerticalCoordinate: number,\n orientation: TreeOrientation = \"horizontal\",\n rootDepthOffset = 0,\n svgWidth = 0,\n depthSpacingScale = 1,\n suppliedItemCountRange?: ItemCountRange,\n): D3Node {\n const treeNodes = treeData.descendants();\n const itemCountRange = suppliedItemCountRange ?? getItemCountRange(\n treeNodes.map((d) => d.data),\n );\n const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);\n if (isVerticalOrientation(orientation)) {\n const maximumLevel = d3Lib.max(treeData.descendants(), (d) => d.depth)!;\n const depthSeparation = layoutSpacings.verticalDepth;\n treeData.each(function (d) {\n d.x = d.x! + highestVerticalCoordinate;\n // \"vertical\": root anchored at the bottom (depthAxisSign -1), depth grows toward y=0.\n // \"vertical-flipped\": root anchored at the top (depthAxisSign +1), depth grows toward\n // maximumLevel * SEPARATION.\n d.y =\n depthAxisSign(orientation) === -1\n ? (maximumLevel - d.depth) * depthSeparation\n : d.depth * depthSeparation + rootDepthOffset;\n });\n return treeData;\n }\n\n if (\n orientation === \"horizontal-anchored\" ||\n orientation === \"horizontal-anchored-flipped\"\n ) {\n // Applied to every node, not just the root: a root-only offset would leave the\n // root→depth-1 gap one rootWidth wider than every later depth-to-depth gap, since it'd\n // shift only one end of that first link. Shifting the whole depth axis instead keeps every\n // gap the uniform HORIZONTAL_SEPARATION_BETWEEN_NODES, while still landing the root's own\n // rendered center on the anchor (see the comment on rootDepthOffset above).\n //\n // \"horizontal-anchored-flipped\" mirrors this around svgWidth instead of 0 — but unlike the\n // non-flipped case, rootDepthOffset must NOT be applied here at all. The `+ width / 2`\n // rendering convention (see renderTree) always shifts a node's center in the +x direction\n // regardless of growth direction, so for the non-flipped anchor (depth grows toward +x) that\n // convention pulls a hidden root's shifted position *back toward* the anchor, and\n // rootDepthOffset must overshoot past it to land half a width behind. For the flipped anchor\n // (depth grows toward -x), that same convention already pushes a hidden root's position\n // *away from* the anchor by exactly one half-width on its own — applying rootDepthOffset on\n // top double-counts that shift and pulls the root a full extra width off the anchor.\n treeData.each(function (d) {\n const tempX = d.x!;\n d.x =\n orientation === \"horizontal-anchored\"\n ? d.y! + rootDepthOffset\n : svgWidth - d.y!;\n d.y = tempX + highestVerticalCoordinate;\n });\n return treeData;\n }\n\n treeData.each(function (d) {\n const tempX = d.x!;\n d.x = d.y!;\n d.y = tempX - highestVerticalCoordinate;\n });\n return treeData;\n}\n\nexport function createTreeLayout(\n d3Lib: typeof import(\"d3\"),\n root: D3Node,\n orientation: TreeOrientation = \"horizontal\",\n depthSpacingScale = 1,\n suppliedItemCountRange?: ItemCountRange,\n): D3Node {\n const nodes = root.descendants();\n const itemCountRange = suppliedItemCountRange ?? getItemCountRange(\n nodes.map((d) => d.data),\n );\n const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);\n const nodeSize: [number, number] = isVerticalOrientation(orientation)\n ? [\n layoutSpacings.verticalSibling,\n layoutSpacings.verticalDepth,\n ]\n : [\n layoutSpacings.verticalBreadth,\n layoutSpacings.horizontalDepth,\n ];\n // d3's default separation() doubles the gap between same-depth nodes that don't share a\n // parent, which compounds up the tree for deeply-branching data and produces gaps several\n // times wider than the nodeSize slot itself. Every node already reserves its own slot via\n // nodeSize, so a flat 1 keeps that slot's spacing consistent regardless of parentage.\n const treeLayout = d3Lib\n .tree<GenreTreeNode>()\n .nodeSize(nodeSize)\n .separation(() => 1);\n return treeLayout(root);\n}\n\nexport function renderTree(\n d3Lib: typeof import(\"d3\"),\n svgRef: React.RefObject<SVGSVGElement>,\n treeData: D3Node,\n svgWidth: number,\n svgHeight: number,\n reparentingNodeId: string | null,\n reparentForbiddenIds: string[],\n rootColor: string,\n callbacks: RenderTreeCallbacks,\n orientation: TreeOrientation = \"horizontal\",\n hideRoot = false,\n selectedNodeId: string | null = null,\n suppliedItemCountRange?: ItemCountRange,\n): D3Selection {\n const { onReparentTargetSelect, onNodeClick } = callbacks;\n\n if (!svgRef.current) {\n throw new Error(\"SVG reference is null\");\n }\n\n const treeNodes = treeData.descendants();\n const itemCountRange = suppliedItemCountRange ?? getItemCountRange(\n treeNodes.map((d) => d.data),\n );\n\n const svg = d3Lib\n .select<SVGSVGElement, unknown>(svgRef.current)\n .append(\"svg\")\n .attr(\"width\", svgWidth)\n .attr(\"height\", svgHeight)\n .style(\"overflow\", \"visible\")\n .append(\"g\") as unknown as D3Selection;\n\n // Card elevation shadow, scoped to this root's id so multiple <GenreTree> instances\n // on one page never collide. The id is sanitized since it's embedded into an SVG\n // `id`/`url(#...)` reference, which arbitrary GenreTreeNode.id strings are not safe for.\n const safeRootId = treeData.data.id.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n const shadowFilterId = `gtv-card-shadow-${safeRootId}`;\n if (ELEVATION) {\n const defs = svg.append(\"defs\");\n const filter = defs\n .append(\"filter\")\n .attr(\"id\", shadowFilterId)\n .attr(\"x\", \"-50%\")\n .attr(\"y\", \"-50%\")\n .attr(\"width\", \"200%\")\n .attr(\"height\", \"200%\");\n filter\n .append(\"feDropShadow\")\n .attr(\"dx\", 0)\n .attr(\"dy\", 0)\n .attr(\"stdDeviation\", 1)\n .attr(\"flood-color\", TEXT_COLOR)\n .attr(\"flood-opacity\", 0.12);\n }\n\n addGrid(svg, svgWidth, svgHeight, true);\n\n appendPaths(\n d3Lib,\n svg,\n treeData,\n itemCountRange,\n orientation,\n selectedNodeId,\n );\n\n const isForbidden = (d: D3Node) => reparentForbiddenIds.includes(d.data.id);\n const isSelected = (d: D3Node) => d.data.id === selectedNodeId;\n // A node is \"related\" to the selected node when it's the selected node's own parent or one of\n // its children — these dim less than the rest of the tree.\n const isRelatedNode = (d: D3Node) =>\n d.parent?.data.id === selectedNodeId ||\n (d.children ?? []).some((c) => c.data.id === selectedNodeId);\n\n // With the actual root hidden, the whole visible subtree grows directly out of the root chip —\n // styling every node the same way (solid root color, bold white label) reads as a continuation\n // of that chip instead of a visual gap into plain cards.\n const isSubtreeCore = (d: D3Node) => hideRoot && d.depth >= 1;\n\n // The hidden root still contributes its (x, y) as the anchor endpoint for appendPaths above —\n // only its own card/toolbar is skipped here, not its position.\n const visibleDescendants = hideRoot\n ? treeData.descendants().filter((d) => d.depth !== 0)\n : treeData.descendants();\n\n const nodes = svg\n .selectAll<SVGGElement, D3Node>(\"g.node\")\n .data(visibleDescendants)\n .enter()\n .append(\"g\")\n .attr(\n \"class\",\n (d) =>\n \"node\" +\n (isForbidden(d) ? \" gtv-node--forbidden\" : \"\") +\n (selectedNodeId && !isSelected(d)\n ? isRelatedNode(d)\n ? \" gtv-node--dimmed-related\"\n : \" gtv-node--dimmed\"\n : \"\"),\n )\n .attr(\"id\", (d) => \"group-\" + d.data.id)\n .attr(\"transform\", function (d) {\n const dimensions = calculateNodeDimensions(\n d.data.itemCount,\n itemCountRange,\n );\n const translateX = d.x! + dimensions.WIDTH / 2;\n const translateY = d.y! + dimensions.HEIGHT / 2;\n return `translate(${translateX}, ${translateY})`;\n })\n // Exposed so the toolbar foreignObject (which overlays the card on hover) can mask the\n // label beneath it with the card's own fill instead of a hardcoded color.\n .style(\"--gtv-node-fill\", (d) =>\n isSubtreeCore(d) ? rootColor : tintSurface(rootColor),\n );\n\n // Invisible hit-region spanning the node body, appended before any visible content so painted\n // siblings (rect, label, toolbar) take pointer-event priority over it wherever they overlap\n // it. <g> itself paints nothing, so without this the group's mouseenter/mouseleave below would\n // be governed only by the union of whichever painted children currently exist.\n nodes\n .append(\"rect\")\n .attr(\"class\", \"gtv-hover-hit-area\")\n .attr(\n \"width\",\n (d) => calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH,\n )\n .attr(\n \"height\",\n (d) => calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT,\n )\n .attr(\n \"x\",\n (d) =>\n -calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH / 2,\n )\n .attr(\n \"y\",\n (d) =>\n -calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT / 2,\n )\n .attr(\"fill\", \"transparent\")\n .attr(\"pointer-events\", \"all\");\n\n // Fill and border are separate paths (rather than one path with both fill and stroke) so that\n // hovering a node can drop just its border's top edge (to merge visually with its hover tab\n // above, which already omits its own border-bottom) without needing a stroke API that can\n // fill without stroking. See the mouseover/mouseleave-timeout handlers below.\n //\n // The card path is wrapped in its own <g> so the elevation filter has a stable target: the\n // hover label is appended outside this group (see the mouseover handler below), so mounting or\n // unmounting it on hover never changes what the filter blurs — the card's shadow always looks\n // the same, hovered or not.\n const cardShadowGroups = nodes\n .append(\"g\")\n .attr(\"class\", \"gtv-card-shadow-group\")\n .attr(\"filter\", ELEVATION ? `url(#${shadowFilterId})` : null);\n\n cardShadowGroups\n .append(\"path\")\n .attr(\"class\", \"gtv-node-rect\")\n .attr(\"d\", (d) => {\n const dimensions = calculateNodeDimensions(\n d.data.itemCount,\n itemCountRange,\n );\n return roundedRectPath(\n -dimensions.WIDTH / 2,\n -dimensions.HEIGHT / 2,\n dimensions.WIDTH,\n dimensions.HEIGHT,\n {\n tl: CORNER_RADIUS,\n tr: CORNER_RADIUS,\n br: CORNER_RADIUS,\n bl: CORNER_RADIUS,\n },\n );\n })\n .attr(\"fill\", (d) =>\n isSubtreeCore(d) ? rootColor : tintSurface(rootColor),\n );\n\n nodes\n .append(\"path\")\n .attr(\"class\", \"gtv-node-border\")\n .attr(\"d\", (d) => {\n const dimensions = calculateNodeDimensions(\n d.data.itemCount,\n itemCountRange,\n );\n return roundedRectPath(\n -dimensions.WIDTH / 2,\n -dimensions.HEIGHT / 2,\n dimensions.WIDTH,\n dimensions.HEIGHT,\n {\n tl: CORNER_RADIUS,\n tr: CORNER_RADIUS,\n br: CORNER_RADIUS,\n bl: CORNER_RADIUS,\n },\n );\n })\n .attr(\"fill\", \"none\")\n .attr(\"stroke\", (d) =>\n isSelected(d) ? ACCENT_COLOR : SURFACE_BORDER_COLOR,\n )\n .attr(\"stroke-width\", (d) =>\n calculateNodeBorderWidth(d.data.itemCount, itemCountRange, isSelected(d)),\n );\n\n nodes\n .append(\"foreignObject\")\n .attr(\n \"width\",\n (d) => calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH,\n )\n .attr(\n \"height\",\n (d) => calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT,\n )\n .attr(\n \"x\",\n (d) =>\n -calculateNodeDimensions(d.data.itemCount, itemCountRange).WIDTH / 2,\n )\n .attr(\n \"y\",\n (d) =>\n -calculateNodeDimensions(d.data.itemCount, itemCountRange).HEIGHT / 2,\n )\n .html((d) => {\n const rootClass =\n (d.depth === 0 || isSubtreeCore(d) ? \" gtv-node-label--root\" : \"\") +\n (d.data.umbrella ? \" gtv-node-label--umbrella\" : \"\");\n const color = isForbidden(d)\n ? TEXT_MUTED_COLOR\n : isSubtreeCore(d)\n ? ACCENT_TEXT_COLOR\n : TEXT_COLOR;\n const fontSize = calculateNodeFontSize(d.data.itemCount, itemCountRange);\n return `<div class=\"gtv-node-label${rootClass}\" style=\"color:${color};font-size:${fontSize}px\">${d.data.name}</div>`;\n });\n\n nodes.each(function (d: D3Node) {\n const group = d3Lib.select<SVGGElement, unknown>(this);\n let leaveTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n group.on(\"mouseenter\", function () {\n // A fast mouse movement between this node's own child shapes (e.g. from the label onto\n // the toolbar) can cross an unpainted gap between them, which the browser reports as\n // briefly leaving and re-entering this <g> (pointer-events only tracks painted areas, and\n // <g> itself paints nothing). Cancel any removal that blip scheduled, or a quick pass-through\n // would still delete the toolbar 100ms later even though the mouse is back on it.\n if (leaveTimeoutId !== null) {\n clearTimeout(leaveTimeoutId);\n leaveTimeoutId = null;\n }\n\n if (reparentingNodeId && d.data.actionable !== false && !isForbidden(d)) {\n addReparentTargetOverlay(\n d3Lib,\n group as unknown as d3.Selection<\n SVGGElement,\n unknown,\n HTMLElement,\n unknown\n >,\n onReparentTargetSelect,\n itemCountRange,\n );\n }\n });\n\n group.on(\"mouseleave\", function () {\n leaveTimeoutId = setTimeout(() => {\n leaveTimeoutId = null;\n d3Lib\n .select<\n SVGGElement,\n unknown\n >(\"#select-as-new-parent-group-\" + d.data.id)\n .remove();\n }, 100);\n });\n\n // Toolbar/menu buttons stopPropagation, so this only fires for a click that actually landed\n // on the card body itself (fill, border, label, or the invisible hit-area). The\n // reparent-target overlay's click doesn't stopPropagation, but it only exists while\n // reparentingNodeId is set, which the guard below already suppresses.\n group.on(\"click\", function (event: MouseEvent) {\n if (reparentingNodeId) return;\n onNodeClick?.(d.data, event);\n });\n });\n\n return svg;\n}\n","import type { ReactNode } from \"react\";\nimport { ZoomControls } from \"@behindthemusictree/ui\";\n\nimport { BusyIndicator } from \"./BusyIndicator\";\nimport type { ControlsOrientation, ZoomControlsVisibility } from \"./types\";\nimport type { UsePanZoomResult } from \"./use-pan-zoom\";\n\ninterface FloatingControlsProps {\n panZoom: Pick<UsePanZoomResult, \"zoomIn\" | \"zoomOut\" | \"canZoomIn\" | \"canZoomOut\">;\n onFit: () => void;\n orientation: ControlsOrientation;\n zoomControls: ZoomControlsVisibility;\n /** Host content laid out beside (horizontal) or above (vertical) the zoom buttons. */\n leading?: ReactNode;\n busy?: boolean;\n}\n\n/** The bottom-right zoom in / zoom out / fit-to-frame buttons, shared by every tree layout. */\nexport function FloatingControls({ panZoom, onFit, orientation, zoomControls, leading, busy }: FloatingControlsProps) {\n return (\n <div\n className={[\n \"gtv-wheel-floating-controls\",\n orientation === \"horizontal\" && \"gtv-wheel-floating-controls--horizontal\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {busy && <BusyIndicator />}\n {leading && <div className=\"gtv-floating-controls-leading\">{leading}</div>}\n {zoomControls !== \"never\" && (\n <ZoomControls\n onZoomIn={panZoom.zoomIn}\n onZoomOut={panZoom.zoomOut}\n onFit={onFit}\n canZoomIn={panZoom.canZoomIn}\n canZoomOut={panZoom.canZoomOut}\n orientation={orientation}\n className={zoomControls === \"auto\" ? \"gtv-zoom-controls gtv-zoom-controls--auto\" : \"gtv-zoom-controls\"}\n />\n )}\n </div>\n );\n}\n","/** Spinner shown in the floating controls while the host marks the tree `busy`. */\nexport function BusyIndicator() {\n return (\n <div className=\"gtv-busy\" role=\"status\" aria-live=\"polite\">\n <span className=\"gtv-busy-ring\" aria-hidden=\"true\" />\n <span className=\"gtv-skeleton-sr-only\">Updating genre tree…</span>\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n CENTER_ON_ELEMENT_DURATION_MS,\n DEFAULT_VIEWPORT_CENTER_GUARD_MS,\n PAN_MIN_VISIBLE_PX,\n PAN_ZOOM_SETTLE_MS,\n ZOOM_ANIMATION_DURATION_MS,\n ZOOM_FIT_PADDING,\n ZOOM_MAX_SCALE,\n ZOOM_MIN_SCALE,\n ZOOM_PINCH_SCALE_SPEED,\n} from \"./constants\";\nimport {\n classifyWheelEvent,\n clampZoomScale,\n computeFitScale,\n computeZoomScale,\n computeZoomScaleForButton,\n createWheelClassifierState,\n viewToViewport,\n viewportToView,\n} from \"./zoom-pan\";\nimport type { ObscuredArea } from \"./info-panel-geometry\";\nimport type { GenreTreeViewport, WheelZoomMode } from \"./types\";\n\nexport interface UsePanZoomOptions {\n defaultViewport?: GenreTreeViewport;\n onViewportChange?: (viewport: GenreTreeViewport) => void;\n}\n\nexport interface UsePanZoomResult {\n panX: number;\n panY: number;\n zoomScale: number;\n /** CSS transform implementing both pan and zoom as one shared coordinate system — apply this\n * to a single \"stage\" element and everything inside it (however many DOM subtrees) moves and\n * scales together with no separate synchronization step. */\n transform: string;\n canZoomIn: boolean;\n canZoomOut: boolean;\n zoomIn: () => void;\n zoomOut: () => void;\n /** Recomputes pan/scale so the union bounding box of the given elements (nulls ignored) fits\n * inside the viewport with ZOOM_FIT_PADDING of clearance. No-ops if none are present/measurable.\n * Also relaxes manual zoom-out's floor to match, when this content needs to go further out\n * than ZOOM_MIN_SCALE — see minScale below. The first successful call applies the hook's\n * `defaultViewport` instead of the fitted pan/scale. `silent` keeps the result from reaching\n * `onViewportChange` — for the automatic fit on mount, as opposed to the fit button. */\n fitToFrame: (elements: (Element | null | undefined)[], options?: { silent?: boolean }) => void;\n /** Animates pan/scale so `element`'s center glides to the center of the viewport's *available*\n * space at `targetScale` (clamped to [minScale, ZOOM_MAX_SCALE]) — used to bring a clicked node\n * to a comfortable reading scale. `targetScale` is a minimum: if the user is already zoomed in\n * past it, the current scale is kept so selecting a node never zooms out. Pass\n * `obscured` when an overlay (e.g. the info panel) will cover part of the viewport, so the\n * element centers within the space that remains visible beside or above it rather than the full\n * viewport. No-ops if the element/viewport isn't present/measurable. */\n centerOnElement: (\n element: Element | null | undefined,\n targetScale: number,\n obscured?: ObscuredArea | null,\n ) => void;\n handlePointerDown: (event: React.PointerEvent) => void;\n /** CSS `touch-action` for the viewport element — see the `wheelZoom` param. */\n touchAction: \"none\" | \"pan-y\";\n}\n\n/**\n * One pan/zoom stage, anchored to `viewportRef` (the element whose bounding rect defines screen\n * space for cursor-anchored zoom and the zoom buttons' own center). Wheel/pinch zooms at the\n * cursor (Google Maps style); click-and-drag over empty background pans. All of it adjusts `panX`/\n * `panY`/`zoomScale` state directly rather than any ancestor's scroll position, so a consumer\n * applying `transform` to one stage element never needs to keep multiple DOM subtrees in sync.\n *\n * `wheelZoom: \"modifier\"` is for viewports embedded in a scrolling page (e.g. a stacked list of\n * trees): only Ctrl/Meta+wheel (and trackpad pinch, which arrives as ctrlKey wheel) zooms, a\n * vertical-dominant plain wheel is left alone so the page scrolls (a horizontal-dominant one still\n * pans X), one-finger touch scrolls the page vertically (`touchAction: \"pan-y\"`) instead of\n * panning the tree, and a two-finger touch pinch zooms the tree.\n *\n * `stageRef` is the element `transform` is applied to. During a gesture the hook writes the\n * transform to it directly, once per frame, and commits `panX`/`panY`/`zoomScale` state (and so\n * re-renders) only when the gesture settles.\n */\nexport function usePanZoom(\n viewportRef: React.RefObject<HTMLElement | null>,\n wheelZoom: WheelZoomMode = \"always\",\n stageRef: React.RefObject<HTMLElement | null>,\n options: UsePanZoomOptions = {},\n): UsePanZoomResult {\n const [panX, setPanX] = useState(0);\n const [panY, setPanY] = useState(0);\n const [zoomScale, setZoomScale] = useState(1);\n\n // Source of truth for pan/zoom while a gesture is in flight; the state above only catches up in\n // commitView, so a burst of wheel/pointer events costs one DOM write per frame, not one React\n // render per event.\n const viewRef = useRef({ x: 0, y: 0, k: 1 });\n const frameIdRef = useRef<number | null>(null);\n const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const stageAnimationRef = useRef<Animation | null>(null);\n const defaultViewportRef = useRef(options.defaultViewport);\n const centerGuardUntilRef = useRef(0);\n const onViewportChangeRef = useRef(options.onViewportChange);\n useEffect(() => {\n onViewportChangeRef.current = options.onViewportChange;\n }, [options.onViewportChange]);\n\n const writeStage = useCallback(() => {\n const stage = stageRef.current;\n if (!stage) return;\n const { x, y, k } = viewRef.current;\n const transform = `translate(${x}px, ${y}px) scale(${k})`;\n stage.classList.add(\"gtv-stage--moving\");\n // Mid-gesture frames go through a held Web Animation rather than style.transform: Chrome\n // re-layerizes every paint chunk of the stage (~8ms per frame on the full pop/core tree) on an\n // inline transform change, but applies an animation's value on the compositor directly.\n if (typeof stage.animate !== \"function\") {\n stage.style.transform = transform;\n return;\n }\n const keyframes = [{ transform }, { transform }];\n if (stageAnimationRef.current) (stageAnimationRef.current.effect as KeyframeEffect).setKeyframes(keyframes);\n else stageAnimationRef.current = stage.animate(keyframes, { duration: Infinity, fill: \"forwards\" });\n }, [stageRef]);\n\n const commitView = useCallback((notify = true) => {\n if (settleTimerRef.current !== null) {\n clearTimeout(settleTimerRef.current);\n settleTimerRef.current = null;\n }\n if (frameIdRef.current !== null) {\n cancelAnimationFrame(frameIdRef.current);\n frameIdRef.current = null;\n }\n const { x, y, k } = viewRef.current;\n const stage = stageRef.current;\n if (stage) {\n stage.style.transform = `translate(${x}px, ${y}px) scale(${k})`;\n stage.classList.remove(\"gtv-stage--moving\");\n }\n stageAnimationRef.current?.cancel();\n stageAnimationRef.current = null;\n setPanX(x);\n setPanY(y);\n setZoomScale(k);\n const viewport = viewportRef.current;\n if (!notify || !onViewportChangeRef.current || !viewport) return;\n const { width, height } = viewport.getBoundingClientRect();\n if (width > 0 && height > 0) onViewportChangeRef.current(viewToViewport({ x, y, k }, width, height));\n }, [stageRef, viewportRef]);\n\n // Tracks every currently-down pointer by id so a second touch landing mid-drag is recognized as\n // the start of a pinch rather than treated as an unrelated pan. Two-finger touch pinch normally\n // never reaches JS at all (the browser treats it as native page zoom) — that's handled by the\n // viewport's touch-action (see `touchAction`; neither \"none\" nor \"pan-y\" allows pinch-zoom),\n // which routes both touch points here as ordinary pointer events instead. Under \"pan-y\" the\n // touchmove listener below additionally stops the browser from claiming a vertically-drifting\n // pinch as a page scroll.\n const activePointersRef = useRef<Map<number, { x: number; y: number }>>(new Map());\n\n // Called after every viewRef mutation from an input event: coalesces the DOM write into one\n // rAF and (re)arms the idle timer that commits state once input stops.\n const scheduleFrame = useCallback(() => {\n if (frameIdRef.current === null) {\n frameIdRef.current = requestAnimationFrame(() => {\n frameIdRef.current = null;\n writeStage();\n });\n }\n // Drag/pinch commit on pointerup: an idle commit mid-drag (a pause while aiming) would re-render,\n // relayout labels and toggle gtv-stage--moving, restyling every node twice.\n if (activePointersRef.current.size > 0) return;\n if (settleTimerRef.current !== null) clearTimeout(settleTimerRef.current);\n settleTimerRef.current = setTimeout(commitView, PAN_ZOOM_SETTLE_MS);\n }, [writeStage, commitView]);\n\n // Floor for manual zoom-out (wheel/pinch/button), relaxed below ZOOM_MIN_SCALE whenever\n // fitToFrame computes a more permissive scale for the current content — so manual zoom-out can\n // always reach at least as far out as \"fit to frame\" does, instead of bottoming out at the\n // static default while fitToFrame jumps straight past it.\n const [minScale, setMinScale] = useState(ZOOM_MIN_SCALE);\n\n // Content bounding box in local (unscaled, pan-independent) coordinates, captured by the most\n // recent fitToFrame call — lets the pan clamp below convert any future panX/panY/zoomScale into\n // the content's on-screen rect without re-measuring the DOM on every drag/wheel event. Stays\n // null until fitToFrame has run at least once; clampPanAxis treats null as \"nothing to clamp\n // against\" and passes the pan value through unchanged.\n const contentBoundsRef = useRef<{ originX: number; originY: number; width: number; height: number } | null>(null);\n\n // Keeps at least PAN_MIN_VISIBLE_PX of content on-screen along each axis, so a drag or wheel-pan\n // can never carry the tree fully out of view with no visible edge left to drag back from. Axes\n // are independent (an X-axis clamp never depends on the current panY, and vice versa), so each\n // can be clamped separately as its own setState updater runs.\n const clampPanAxis = useCallback((pan: number, scale: number, axis: \"x\" | \"y\") => {\n const viewport = viewportRef.current;\n const bounds = contentBoundsRef.current;\n if (!viewport || !bounds) return pan;\n\n const origin = axis === \"x\" ? bounds.originX : bounds.originY;\n const size = axis === \"x\" ? bounds.width : bounds.height;\n // clientWidth/clientHeight (not getBoundingClientRect) since this runs on every wheel/pointer-\n // move event and only the viewport's own size is needed, not its position — avoids forcing a\n // synchronous layout read in a hot path.\n const viewportSize = axis === \"x\" ? viewport.clientWidth : viewport.clientHeight;\n\n const panMax = viewportSize - PAN_MIN_VISIBLE_PX - origin * scale;\n const panMin = PAN_MIN_VISIBLE_PX - size * scale - origin * scale;\n const lo = Math.min(panMin, panMax);\n const hi = Math.max(panMin, panMax);\n return Math.min(hi, Math.max(lo, pan));\n }, [viewportRef]);\n\n // Reads/writes viewRef.current (not the zoomScale/minScale state closures) so a burst of\n // events firing faster than React can commit a render — a trackpad delivers many wheel ticks\n // per gesture, a touch pinch many pointermoves per frame — each sees the immediately-preceding\n // event's result instead of a stale pre-render scale. Without this, rapid events either collapse\n // onto one stale base (perceived as sluggish/laggy trackpad zoom) or anchor against the wrong\n // scale (perceived as the zoomed point drifting during a fast pinch).\n const zoomAtPoint = useCallback(\n (computeNewScale: (currentScale: number) => number, clientX: number, clientY: number) => {\n const viewport = viewportRef.current;\n if (!viewport) {\n return;\n }\n const view = viewRef.current;\n const currentScale = view.k;\n const newScale = computeNewScale(currentScale);\n if (newScale === currentScale) {\n return;\n }\n\n // A wheel/pinch zoom mid-glide takes over from centerOnElement's animation immediately,\n // rather than fighting it over the same panX/panY/zoomScale state.\n centerAnimationRef.current = null;\n\n const rect = viewport.getBoundingClientRect();\n const contentX = (clientX - rect.left - view.x) / currentScale;\n const contentY = (clientY - rect.top - view.y) / currentScale;\n view.x = clientX - rect.left - contentX * newScale;\n view.y = clientY - rect.top - contentY * newScale;\n view.k = newScale;\n scheduleFrame();\n },\n [viewportRef, scheduleFrame],\n );\n\n // Tracks the ctrl+wheel glide animation started by animateZoomTo below — null when no\n // animation is in flight. `targetScale` (not the already-reached interpolated scale) is what a\n // new wheel event during an in-flight glide extends, so a burst of ticks compounds into one\n // continuous glide-and-settle instead of restarting/jumping on every event, matching Google\n // Maps' feel.\n const zoomAnimationRef = useRef<{\n startScale: number;\n targetScale: number;\n startTime: number;\n clientX: number;\n clientY: number;\n } | null>(null);\n const animationFrameIdRef = useRef<number | null>(null);\n // Running state for classifyWheelEvent (see zoom-pan.ts), distinguishing a physical mouse\n // wheel's few large ticks from a trackpad ctrl+wheel gesture's many small ones so each gets its\n // own response curve.\n const wheelClassifierRef = useRef(createWheelClassifierState());\n\n // stepZoomAnimation schedules itself for the next frame, so it needs to call its own latest\n // version without referencing the `const` it's assigned to inside its own body (disallowed by\n // the hooks linter, mirroring stablePointerMove/handlePointerMoveRef's ref-indirection below).\n const stepZoomAnimationRef = useRef<() => void>(() => {});\n\n const stepZoomAnimation = useCallback(() => {\n const anim = zoomAnimationRef.current;\n const viewport = viewportRef.current;\n if (!anim || !viewport) {\n animationFrameIdRef.current = null;\n return;\n }\n\n const t = Math.min(1, (performance.now() - anim.startTime) / ZOOM_ANIMATION_DURATION_MS);\n const eased = 1 - Math.pow(1 - t, 3);\n const view = viewRef.current;\n const currentScale = view.k;\n const frameScale = anim.startScale + (anim.targetScale - anim.startScale) * eased;\n\n // Re-anchored every frame (not just once at the start) using the current frame's interpolated\n // scale, so the same content point stays under the cursor throughout the glide rather than\n // only at the end.\n const rect = viewport.getBoundingClientRect();\n const contentX = (anim.clientX - rect.left - view.x) / currentScale;\n const contentY = (anim.clientY - rect.top - view.y) / currentScale;\n view.x = anim.clientX - rect.left - contentX * frameScale;\n view.y = anim.clientY - rect.top - contentY * frameScale;\n view.k = frameScale;\n writeStage();\n\n if (t < 1) {\n animationFrameIdRef.current = requestAnimationFrame(() => stepZoomAnimationRef.current());\n } else {\n zoomAnimationRef.current = null;\n animationFrameIdRef.current = null;\n commitView();\n }\n }, [viewportRef, writeStage, commitView]);\n useEffect(() => {\n stepZoomAnimationRef.current = stepZoomAnimation;\n }, [stepZoomAnimation]);\n\n // Entry point for the animated ctrl+wheel zoom path. Retargets an in-flight glide (extending\n // its target and restarting the ease from the current interpolated scale) rather than letting\n // overlapping animations fight, so a fast burst of wheel ticks reads as one continuous glide.\n const animateZoomTo = useCallback(\n (computeNewScale: (currentScale: number) => number, clientX: number, clientY: number) => {\n const inFlight = zoomAnimationRef.current;\n const baseScale = inFlight ? inFlight.targetScale : viewRef.current.k;\n const targetScale = computeNewScale(baseScale);\n if (targetScale === baseScale) {\n return;\n }\n\n // A physical-wheel zoom mid-glide takes over from centerOnElement's animation immediately,\n // rather than fighting it over the same panX/panY/zoomScale state.\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = null;\n\n zoomAnimationRef.current = {\n startScale: viewRef.current.k,\n targetScale,\n startTime: performance.now(),\n clientX,\n clientY,\n };\n\n if (animationFrameIdRef.current === null) {\n animationFrameIdRef.current = requestAnimationFrame(stepZoomAnimation);\n }\n },\n [stepZoomAnimation],\n );\n\n // Keeps the viewport's current center point fixed on screen when its box is resized (matching\n // Google Maps: resizing the map container never re-fits or re-zooms, it just reveals/hides edges\n // around the same center) — panX/panY shift by half the size delta so the point that was in the\n // middle of the old box is still in the middle of the new one, then get re-clamped in case the\n // resize shrank the viewport below what the current pan allows.\n const viewportSizeRef = useRef<{ width: number; height: number } | null>(null);\n useEffect(() => {\n const viewport = viewportRef.current;\n if (!viewport) return;\n\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const { width, height } = entry.contentRect;\n\n const previous = viewportSizeRef.current;\n viewportSizeRef.current = { width, height };\n if (!previous) return;\n\n const dx = (width - previous.width) / 2;\n const dy = (height - previous.height) / 2;\n if (dx === 0 && dy === 0) return;\n\n const view = viewRef.current;\n view.x = clampPanAxis(view.x + dx, view.k, \"x\");\n view.y = clampPanAxis(view.y + dy, view.k, \"y\");\n commitView();\n });\n observer.observe(viewport);\n return () => observer.disconnect();\n }, [viewportRef, clampPanAxis, commitView]);\n\n // Non-passive + attached directly to the DOM node (rather than React's onWheel) because\n // React's wheel handler is passive by default, which silently drops preventDefault() — and\n // without it, ctrl+wheel triggers the browser's own page zoom instead of this one. Registered\n // once (zoomAtPoint/clampPanAxis read live scale via refs rather than being deps here) instead\n // of re-subscribing on every scale change, which previously tore down and rebuilt this listener\n // on every single wheel tick during a continuous trackpad gesture.\n useEffect(() => {\n const viewport = viewportRef.current;\n if (!viewport) {\n return;\n }\n\n const handleWheel = (event: WheelEvent) => {\n // Let the info panel handle its own scroll (it has overflow-y: auto) instead of panning/\n // zooming the tree underneath it — it's rendered as a child of this same viewport, so its\n // wheel events would otherwise bubble up here.\n if ((event.target as Element).closest(\".gtv-info-panel\") !== null) {\n return;\n }\n if (wheelZoom === \"modifier\" && !event.ctrlKey && !event.metaKey) {\n // A horizontal-dominant swipe has no page scroll to yield to — passing it through would\n // leave the tree un-pannable sideways and let macOS turn it into back/forward navigation.\n if (Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return;\n event.preventDefault();\n const view = viewRef.current;\n view.x = clampPanAxis(view.x - event.deltaX, view.k, \"x\");\n scheduleFrame();\n return;\n }\n event.preventDefault();\n const now = performance.now();\n const wheelType = classifyWheelEvent(event.deltaY, now, wheelClassifierRef.current);\n if (wheelType === \"trackpad\") {\n // Trackpad scroll/pinch already arrives as a continuous, high-frequency stream of\n // small deltas — applying it instantly (like Google Maps/MapLibre do) tracks the\n // gesture 1:1 with zero added latency. Easing *each* of these events (as the \"wheel\"\n // branch below does) stacked a ~200ms lag behind every single one, which read as the\n // trackpad path lagging noticeably behind Google Maps' own feel.\n zoomAtPoint((current) => computeZoomScale(current, event.deltaY, minScaleRef.current), event.clientX, event.clientY);\n } else {\n // A physical wheel's notches are few and chunky (one big jump per click), so easing the\n // glide between them reads as smooth rather than laggy.\n animateZoomTo(\n (current) => computeZoomScaleForButton(current, event.deltaY < 0 ? 1 : -1, minScaleRef.current),\n event.clientX,\n event.clientY,\n );\n }\n };\n\n viewport.addEventListener(\"wheel\", handleWheel, { passive: false });\n return () => viewport.removeEventListener(\"wheel\", handleWheel);\n }, [animateZoomTo, zoomAtPoint, viewportRef, clampPanAxis, scheduleFrame, wheelZoom]);\n\n // Under \"pan-y\" the browser may claim a two-finger pinch whose fingers drift vertically as a page\n // scroll (firing pointercancel and killing the pinch). Cancelling touchmove once a second finger\n // is down keeps the gesture ours, while a lone finger still scrolls the page — the same\n // cooperative-gesture split Google Maps uses for embedded maps. Non-passive for the same reason\n // as the wheel listener above.\n useEffect(() => {\n const viewport = viewportRef.current;\n if (!viewport || wheelZoom !== \"modifier\") return;\n const handleTouchMove = (event: TouchEvent) => {\n if (event.touches.length >= 2) event.preventDefault();\n };\n viewport.addEventListener(\"touchmove\", handleTouchMove, { passive: false });\n return () => viewport.removeEventListener(\"touchmove\", handleTouchMove);\n }, [viewportRef, wheelZoom]);\n\n // Fallback for input that never reaches the wheel handler above — e.g. a trackpad/OS/browser\n // combination that doesn't translate a pinch gesture into a ctrlKey wheel event at all.\n // Anchored on the viewport's own center.\n const zoomByButton = useCallback(\n (direction: 1 | -1) => {\n const viewport = viewportRef.current;\n if (!viewport) return;\n const rect = viewport.getBoundingClientRect();\n zoomAtPoint(\n (current) => computeZoomScaleForButton(current, direction, minScaleRef.current),\n rect.left + rect.width / 2,\n rect.top + rect.height / 2,\n );\n commitView();\n },\n [zoomAtPoint, viewportRef, commitView],\n );\n\n // Generalizes zoomAtPoint's screen->content conversion from a single point to the union\n // bounding box of one or more elements, measured live via getBoundingClientRect() — this way\n // fitting the wheel+tree doesn't require re-deriving their geometry (radius, chip offsets,\n // svg dimensions) a second time here.\n const fitToFrame = useCallback(\n (elements: (Element | null | undefined)[], { silent = false }: { silent?: boolean } = {}) => {\n const viewport = viewportRef.current;\n const present = elements.filter((el): el is Element => Boolean(el));\n if (!viewport || present.length === 0) return;\n\n const viewportRect = viewport.getBoundingClientRect();\n if (viewportRect.width <= 0 || viewportRect.height <= 0) return;\n\n // fitToFrame sets panX/panY/zoomScale directly, so any in-flight centerOnElement glide would\n // otherwise resume overwriting it on its very next frame.\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = null;\n // computeFitScale subtracts ZOOM_FIT_PADDING*2 from each dimension and deliberately never\n // clamps its lower bound (see zoom-pan.ts) — a viewport too small to hold the padding alone\n // would drive the result negative, mirroring/flinging the tree out of frame. Bail out rather\n // than fit into a viewport that can't even fit the padding.\n if (viewportRect.width <= ZOOM_FIT_PADDING * 2 || viewportRect.height <= ZOOM_FIT_PADDING * 2) return;\n\n const rects = present.map((el) => el.getBoundingClientRect());\n const contentLeft = Math.min(...rects.map((r) => r.left));\n const contentTop = Math.min(...rects.map((r) => r.top));\n const contentRight = Math.max(...rects.map((r) => r.right));\n const contentBottom = Math.max(...rects.map((r) => r.bottom));\n\n const view = viewRef.current;\n const contentWidth = (contentRight - contentLeft) / view.k;\n const contentHeight = (contentBottom - contentTop) / view.k;\n if (contentWidth <= 0 || contentHeight <= 0) return;\n\n const contentOriginX = (contentLeft - viewportRect.left - view.x) / view.k;\n const contentOriginY = (contentTop - viewportRect.top - view.y) / view.k;\n\n contentBoundsRef.current = {\n originX: contentOriginX,\n originY: contentOriginY,\n width: contentWidth,\n height: contentHeight,\n };\n\n const fitScale = computeFitScale(\n contentWidth,\n contentHeight,\n viewportRect.width,\n viewportRect.height,\n ZOOM_FIT_PADDING,\n );\n\n // Keep viewRef/minScaleRef current immediately so a wheel/pinch event firing right after\n // fitToFrame reads the new scale instead of a stale one from before this render commits.\n const newMinScale = Math.min(ZOOM_MIN_SCALE, fitScale);\n // eslint-disable-next-line react-hooks/immutability -- see comment above\n minScaleRef.current = newMinScale;\n setMinScale(newMinScale);\n const seed = defaultViewportRef.current;\n if (seed) {\n defaultViewportRef.current = undefined;\n const seeded = viewportToView(\n { ...seed, zoom: clampZoomScale(seed.zoom, newMinScale) },\n viewportRect.width,\n viewportRect.height,\n );\n view.k = seeded.k;\n view.x = seeded.x;\n view.y = seeded.y;\n centerGuardUntilRef.current = performance.now() + DEFAULT_VIEWPORT_CENTER_GUARD_MS;\n commitView(false);\n return;\n }\n view.k = fitScale;\n view.x = viewportRect.width / 2 - (contentOriginX + contentWidth / 2) * fitScale;\n view.y = viewportRect.height / 2 - (contentOriginY + contentHeight / 2) * fitScale;\n commitView(!silent);\n },\n [viewportRef, commitView],\n );\n\n // Tracks the eased \"fly to\" glide started by centerOnElement below — unlike zoomAnimationRef's\n // wheel-notch glide (which re-anchors around a fixed *screen* point every frame), this eases\n // panX/panY/zoomScale together toward one fixed target, since the destination here is \"this\n // element's content-space center on screen\", not \"whatever's under the cursor right now\" —\n // matching the single smooth flight of Google Maps' own click-to-center-and-zoom.\n const centerAnimationRef = useRef<{\n startPanX: number;\n startPanY: number;\n startScale: number;\n targetPanX: number;\n targetPanY: number;\n targetScale: number;\n startTime: number;\n } | null>(null);\n const centerAnimationFrameIdRef = useRef<number | null>(null);\n // Ref-indirection for the same reason stepZoomAnimationRef needs it above: this schedules its\n // own next frame and can't reference the `const` it's assigned to from inside its own body.\n const stepCenterAnimationRef = useRef<() => void>(() => {});\n\n const stepCenterAnimation = useCallback(() => {\n const anim = centerAnimationRef.current;\n if (!anim) {\n centerAnimationFrameIdRef.current = null;\n return;\n }\n\n const t = Math.min(1, (performance.now() - anim.startTime) / CENTER_ON_ELEMENT_DURATION_MS);\n const eased = 1 - Math.pow(1 - t, 3);\n const scale = anim.startScale + (anim.targetScale - anim.startScale) * eased;\n const px = anim.startPanX + (anim.targetPanX - anim.startPanX) * eased;\n const py = anim.startPanY + (anim.targetPanY - anim.startPanY) * eased;\n\n const view = viewRef.current;\n view.k = scale;\n view.x = px;\n view.y = py;\n writeStage();\n\n if (t < 1) {\n centerAnimationFrameIdRef.current = requestAnimationFrame(() => stepCenterAnimationRef.current());\n } else {\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = null;\n centerAnimationFrameIdRef.current = null;\n commitView();\n }\n }, [writeStage, commitView]);\n useEffect(() => {\n stepCenterAnimationRef.current = stepCenterAnimation;\n }, [stepCenterAnimation]);\n\n // Same screen->content conversion as fitToFrame/zoomAtPoint, but targeting one element's center\n // at a fixed scale rather than fitting a bounding box — used for \"click a node, bring it to a\n // comfortable reading scale, centered\" instead of \"fit everything on screen\". Glides there via\n // stepCenterAnimation rather than jumping instantly.\n const centerOnElement = useCallback(\n (\n element: Element | null | undefined,\n targetScale: number,\n obscured?: ObscuredArea | null,\n ) => {\n const viewport = viewportRef.current;\n if (!viewport || !element || performance.now() < centerGuardUntilRef.current) return;\n\n const viewportRect = viewport.getBoundingClientRect();\n if (viewportRect.width <= 0 || viewportRect.height <= 0) return;\n\n const rect = element.getBoundingClientRect();\n // Reads viewRef/minScaleRef (not the zoomScale/minScale/panX/panY state closures) for the same reason zoomAtPoint does: this is invoked from a D3 click handler\n // rebound only when a largely-unrelated effect's deps change, so a state closure here would\n // compute against whatever pan/zoom was current when that effect last ran rather than the\n // live value.\n const view = viewRef.current;\n const currentScale = view.k;\n const newScale = clampZoomScale(Math.max(targetScale, currentScale), minScaleRef.current);\n const centerX = (rect.left + rect.width / 2 - viewportRect.left - view.x) / currentScale;\n const centerY = (rect.top + rect.height / 2 - viewportRect.top - view.y) / currentScale;\n // With an overlay obscuring one side, \"centered on screen\" means centered in the strip that\n // remains visible beside (or above) it, not the full viewport — otherwise the element ends\n // up right where the overlay covers it.\n const availableCenterX =\n obscured?.side === \"left\"\n ? obscured.width + (viewportRect.width - obscured.width) / 2\n : obscured?.side === \"right\"\n ? (viewportRect.width - obscured.width) / 2\n : viewportRect.width / 2;\n const availableCenterY =\n obscured?.side === \"bottom\" ? (viewportRect.height - obscured.height) / 2 : viewportRect.height / 2;\n const targetPanX = availableCenterX - centerX * newScale;\n const targetPanY = availableCenterY - centerY * newScale;\n\n // Cancel any in-flight wheel-notch glide so it doesn't fight this animation over the same\n // panX/panY/zoomScale state.\n zoomAnimationRef.current = null;\n if (animationFrameIdRef.current !== null) {\n cancelAnimationFrame(animationFrameIdRef.current);\n animationFrameIdRef.current = null;\n }\n\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = {\n startPanX: view.x,\n startPanY: view.y,\n startScale: currentScale,\n targetPanX,\n targetPanY,\n targetScale: newScale,\n startTime: performance.now(),\n };\n if (centerAnimationFrameIdRef.current === null) {\n centerAnimationFrameIdRef.current = requestAnimationFrame(() => stepCenterAnimationRef.current());\n }\n },\n [viewportRef],\n );\n\n // Click-and-drag pan over empty background. Only starts outside a node/its toolbar so it\n // doesn't fight their own click/hover interactions.\n const lastPointRef = useRef({ x: 0, y: 0 });\n\n // handlePointerDown/handlePointerMove/handlePointerUp are registered as window listeners via\n // referentially-stable wrappers (see stablePointerMove/stablePointerUp below), so they can't just\n // read minScale directly — that would freeze it at whatever value was current when the listener\n // was attached. Mirroring it into a ref gives the closures a live read instead; every\n // setMinScale call site updates its ref in the same spot, so there's no separate effect racing\n // those writes.\n const minScaleRef = useRef(minScale);\n\n // Pointers whose pointerdown landed on a node/toolbar/control, or (in \"modifier\" mode) any touch\n // pointer: excluded from single-pointer pan (so they don't fight that element's own click/hover\n // handling, or the page's own vertical scroll) but still tracked so a pinch that starts on top of\n // content — the common case, since the tree fills most of the screen — still works.\n const suppressedPointersRef = useRef<Set<number>>(new Set());\n // Also remembers which pointer ids the current baseline was computed from, so a third finger\n // landing (or the active pair otherwise changing) recomputes it instead of reusing a stale\n // distance/scale from a different pair — which would otherwise cause a sudden jump in zoom.\n const pinchStartRef = useRef<{ ids: [number, number]; distance: number; scale: number } | null>(null);\n\n const pinchPointerIds = useCallback((pointers: Map<number, { x: number; y: number }>): [number, number] => {\n const ids = Array.from(pointers.keys());\n return [ids[0], ids[1]];\n }, []);\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n const pointers = activePointersRef.current;\n if (!pointers.has(event.pointerId)) return;\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });\n\n if (pointers.size >= 2) {\n const [idA, idB] = pinchPointerIds(pointers);\n const a = pointers.get(idA)!;\n const b = pointers.get(idB)!;\n const distance = Math.hypot(a.x - b.x, a.y - b.y);\n const current = pinchStartRef.current;\n if (!current || current.ids[0] !== idA || current.ids[1] !== idB) {\n pinchStartRef.current = { ids: [idA, idB], distance, scale: viewRef.current.k };\n return;\n }\n if (current.distance <= 0) {\n return;\n }\n zoomAtPoint(\n () =>\n clampZoomScale(\n current.scale * Math.pow(distance / current.distance, ZOOM_PINCH_SCALE_SPEED),\n minScaleRef.current,\n ),\n (a.x + b.x) / 2,\n (a.y + b.y) / 2,\n );\n return;\n }\n\n if (suppressedPointersRef.current.has(event.pointerId)) return;\n\n const dx = event.clientX - lastPointRef.current.x;\n const dy = event.clientY - lastPointRef.current.y;\n lastPointRef.current = { x: event.clientX, y: event.clientY };\n const view = viewRef.current;\n view.x = clampPanAxis(view.x + dx, view.k, \"x\");\n view.y = clampPanAxis(view.y + dy, view.k, \"y\");\n scheduleFrame();\n },\n [zoomAtPoint, clampPanAxis, pinchPointerIds, scheduleFrame],\n );\n\n // handlePointerMove/handlePointerUp are recreated whenever zoomAtPoint (and so zoomScale)\n // changes, e.g. mid-pinch — but window.addEventListener/removeEventListener only match by\n // function identity. Registering these referentially stable wrappers instead (which delegate to\n // the latest handler via ref) means the exact same function passed to addEventListener is always\n // the one passed to removeEventListener, so a pinch never leaves a stale listener attached.\n const handlePointerMoveRef = useRef(handlePointerMove);\n useEffect(() => {\n handlePointerMoveRef.current = handlePointerMove;\n }, [handlePointerMove]);\n const handlePointerUpRef = useRef<(event: PointerEvent) => void>(() => {});\n const stablePointerMove = useCallback((event: PointerEvent) => handlePointerMoveRef.current(event), []);\n const stablePointerUp = useCallback((event: PointerEvent) => handlePointerUpRef.current(event), []);\n\n const handlePointerUp = useCallback((event: PointerEvent) => {\n const pointers = activePointersRef.current;\n pointers.delete(event.pointerId);\n suppressedPointersRef.current.delete(event.pointerId);\n\n if (pointers.size < 2) pinchStartRef.current = null;\n const remaining = Array.from(pointers.values());\n // Dropping from two fingers to one resumes as a plain drag, anchored at the finger left on\n // screen, instead of jumping by the distance between the old two-finger midpoint and it.\n if (remaining.length === 1) lastPointRef.current = remaining[0];\n\n if (pointers.size === 0) {\n window.removeEventListener(\"pointermove\", stablePointerMove);\n window.removeEventListener(\"pointerup\", stablePointerUp);\n window.removeEventListener(\"pointercancel\", stablePointerUp);\n commitView();\n }\n }, [stablePointerMove, stablePointerUp, commitView]);\n useEffect(() => {\n handlePointerUpRef.current = handlePointerUp;\n }, [handlePointerUp]);\n\n const handlePointerDown = useCallback((event: React.PointerEvent) => {\n if (event.button !== 0) return;\n // A pointer landing on a node/toolbar/control/info panel is excluded from single-pointer pan (so\n // it doesn't fight that element's own click/hover/text-selection handling) but still tracked below — otherwise a\n // pinch that starts on top of content, the common case since the tree fills most of the screen,\n // would never be recognized as a pinch at all.\n const isInteractive = (event.target as Element).closest(\"g.node, foreignObject, .gtv-zoom-controls, .gtv-wheel-chip, .gtv-info-panel\") !== null;\n // In \"modifier\" mode one finger scrolls the page, so a touch pointer must neither pan (which\n // nudges the tree until the browser fires pointercancel) nor preventDefault the scroll.\n const isPageScrollTouch = wheelZoom === \"modifier\" && event.pointerType === \"touch\";\n\n const pointers = activePointersRef.current;\n const wasEmpty = pointers.size === 0;\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });\n lastPointRef.current = { x: event.clientX, y: event.clientY };\n\n if (isInteractive || isPageScrollTouch) {\n suppressedPointersRef.current.add(event.pointerId);\n } else {\n event.preventDefault();\n // Starting a background drag takes over from any in-flight centerOnElement glide, rather\n // than fighting it over the same panX/panY state.\n // eslint-disable-next-line react-hooks/immutability -- see minScaleRef comment in fitToFrame\n centerAnimationRef.current = null;\n }\n\n if (wasEmpty) {\n window.addEventListener(\"pointermove\", stablePointerMove);\n window.addEventListener(\"pointerup\", stablePointerUp);\n window.addEventListener(\"pointercancel\", stablePointerUp);\n }\n }, [stablePointerMove, stablePointerUp, wheelZoom]);\n\n // Covers the case handlePointerUp's own cleanup can't: the component unmounting mid-gesture\n // (route change, conditional render) before every pointer has lifted, which would otherwise\n // leave these window listeners attached and still calling setState after unmount.\n useEffect(() => {\n const pointers = activePointersRef.current;\n const suppressed = suppressedPointersRef.current;\n return () => {\n window.removeEventListener(\"pointermove\", stablePointerMove);\n window.removeEventListener(\"pointerup\", stablePointerUp);\n window.removeEventListener(\"pointercancel\", stablePointerUp);\n pointers.clear();\n suppressed.clear();\n pinchStartRef.current = null;\n if (animationFrameIdRef.current !== null) cancelAnimationFrame(animationFrameIdRef.current);\n if (centerAnimationFrameIdRef.current !== null) cancelAnimationFrame(centerAnimationFrameIdRef.current);\n if (frameIdRef.current !== null) cancelAnimationFrame(frameIdRef.current);\n if (settleTimerRef.current !== null) clearTimeout(settleTimerRef.current);\n };\n }, [stablePointerMove, stablePointerUp]);\n\n return {\n panX,\n panY,\n zoomScale,\n transform: `translate(${panX}px, ${panY}px) scale(${zoomScale})`,\n canZoomIn: zoomScale < ZOOM_MAX_SCALE,\n canZoomOut: zoomScale > minScale,\n zoomIn: () => zoomByButton(1),\n zoomOut: () => zoomByButton(-1),\n fitToFrame,\n centerOnElement,\n handlePointerDown,\n touchAction: wheelZoom === \"modifier\" ? \"pan-y\" : \"none\",\n };\n}\n","import {\n ZOOM_BUTTON_SCALE_STEP,\n ZOOM_MAX_SCALE,\n ZOOM_MIN_SCALE,\n ZOOM_WHEEL_MAX_EXPONENT,\n ZOOM_WHEEL_SCALE_SPEED,\n} from \"./constants\";\nimport type { GenreTreeViewport } from \"./types\";\n\n/** `minScale` defaults to ZOOM_MIN_SCALE but accepts an override so manual zoom-out's floor can\n * be relaxed to match a \"fit to frame\" scale computed for content too large for the static\n * default to reach — see usePanZoom's dynamic min-scale tracking. */\nexport function clampZoomScale(scale: number, minScale: number = ZOOM_MIN_SCALE): number {\n return Math.min(ZOOM_MAX_SCALE, Math.max(minScale, scale));\n}\n\nexport interface PanZoomView {\n x: number;\n y: number;\n k: number;\n}\n\nexport function viewToViewport(view: PanZoomView, width: number, height: number): GenreTreeViewport {\n return { x: (width / 2 - view.x) / view.k, y: (height / 2 - view.y) / view.k, zoom: view.k };\n}\n\nexport function viewportToView(viewport: GenreTreeViewport, width: number, height: number): PanZoomView {\n return { x: width / 2 - viewport.x * viewport.zoom, y: height / 2 - viewport.y * viewport.zoom, k: viewport.zoom };\n}\n\n/** Exponential response to wheel delta so the zoom feels proportional at any current scale —\n * a fixed step would feel abrupt when zoomed far out and sluggish when zoomed far in. The exponent\n * is clamped to ±ZOOM_WHEEL_MAX_EXPONENT so an unusually fast swipe (large |wheelDeltaY| in one\n * event) can't produce a runaway multiplicative jump — see that constant's doc comment. */\nexport function computeZoomScale(currentScale: number, wheelDeltaY: number, minScale: number = ZOOM_MIN_SCALE): number {\n const exponent = Math.max(\n -ZOOM_WHEEL_MAX_EXPONENT,\n Math.min(ZOOM_WHEEL_MAX_EXPONENT, -wheelDeltaY * ZOOM_WHEEL_SCALE_SPEED),\n );\n return clampZoomScale(currentScale * Math.exp(exponent), minScale);\n}\n\nexport function computeZoomScaleForButton(currentScale: number, direction: 1 | -1, minScale: number = ZOOM_MIN_SCALE): number {\n return clampZoomScale(currentScale * Math.pow(ZOOM_BUTTON_SCALE_STEP, direction), minScale);\n}\n\n/** Cross-browser quantum a physical mouse wheel's deltaY always lands on exact multiples of\n * (a long-documented Firefox/Chrome quirk); trackpad-generated deltaY values essentially never\n * do. Used by classifyWheelEvent below. */\nconst WHEEL_TICK_DELTA = 4.000244140625;\n\nexport interface WheelClassifierState {\n lastTime: number;\n lastType: \"wheel\" | \"trackpad\" | null;\n /** True after a gesture's first ambiguous event, whose timeDelta is the gap since the previous\n * gesture rather than a real event rate — the gesture's second event re-decides from its rate. */\n provisional: boolean;\n}\n\nexport function createWheelClassifierState(): WheelClassifierState {\n return { lastTime: 0, lastType: null, provisional: false };\n}\n\n/** Classifies a ctrl+wheel event as a physical mouse wheel (few large, quantized deltaY steps) or\n * a trackpad ctrl+wheel/pinch gesture (many small deltaY events) — adapted from MapLibre/Mapbox\n * GL's scroll-zoom heuristic, so each can get its own response curve instead of sharing one.\n * `state` carries the running classification across a gesture's repeated events and is mutated\n * in place. */\nexport function classifyWheelEvent(deltaY: number, now: number, state: WheelClassifierState): \"wheel\" | \"trackpad\" {\n const timeDelta = now - state.lastTime;\n state.lastTime = now;\n\n let type: \"wheel\" | \"trackpad\";\n if (deltaY !== 0 && deltaY % WHEEL_TICK_DELTA === 0) {\n // Definitely a mouse wheel: this exact quantization essentially never occurs by chance.\n type = \"wheel\";\n } else if (Math.abs(deltaY) < 4) {\n // Definitely a trackpad: too small to be a real wheel notch.\n type = \"trackpad\";\n } else if (timeDelta > 400 || !state.lastType || state.provisional) {\n // Infer from delta-per-time — a fast trackpad still moves less per ms than a wheel notch does.\n // A new gesture's first event only gets a provisional guess (see `provisional`), as in\n // MapLibre, which otherwise labels nearly every trackpad scroll \"wheel\" from its first event.\n type = Math.abs(timeDelta * deltaY) < 200 ? \"trackpad\" : \"wheel\";\n state.provisional = timeDelta > 400 || !state.lastType;\n } else {\n // Ambiguous repeat within the same gesture: stay with whatever this gesture already is.\n type = state.lastType;\n }\n\n state.lastType = type;\n return type;\n}\n\n/** Selector for the elements \"fit to frame\" should measure: the tree's actually-rendered cards\n * (or radial dots and their labels) and connector links. Deliberately excludes the invisible per-node hit area and the SVG's own\n * declared width/height, both of which bake in ACTIONS_OVERLAY_WIDTH/HEIGHT (see constants.ts) —\n * toolbar/menu clearance reserved to one side of the tree. Fitting to that reserved space instead\n * of the visible content leaves a large, lopsided gap around the tree after fitting. */\nexport const TREE_CONTENT_SELECTOR = \".gtv-node-rect, .gtv-node-dot, text.gtv-node-label, .gtv-link\";\n\n/** Collects the tight-content elements (see TREE_CONTENT_SELECTOR) under `container`, ready to\n * pass into usePanZoom's fitToFrame. */\nexport function queryTreeContentElements(container: Element | null | undefined): Element[] {\n if (!container) return [];\n return Array.from(container.querySelectorAll(TREE_CONTENT_SELECTOR));\n}\n\n/** Scale that fits `contentWidth`×`contentHeight` inside `viewportWidth`×`viewportHeight` with\n * `padding` px of clearance on every side, picking whichever dimension is more constraining.\n * Capped at 1 (never zooms in past the tree's natural size), not just `ZOOM_MAX_SCALE`: content is\n * measured from `TREE_CONTENT_SELECTOR` alone, excluding the ACTIONS_OVERLAY_WIDTH/HEIGHT space\n * the layout reserves for toolbars/menus. That reserved space scales together with the tree\n * under the shared zoom transform, so scaling past 1 would inflate it past the fixed-px `padding`\n * and clip an edge node's toolbar/menu against the viewport. At scale ≤ 1 the layout's own reserved\n * space is never exceeded, so clipping can't happen. This deliberately ignores `ZOOM_MIN_SCALE`:\n * capping it there would make the button lie about \"fitting\" content too large to fit at that\n * static floor, rendering it too big for the viewport while still centering on the full (larger)\n * bounding box, cropping content. usePanZoom instead relaxes its own interactive floor to match,\n * so manual zoom-out can always reach at least as far out as this computes. */\nexport function computeFitScale(\n contentWidth: number,\n contentHeight: number,\n viewportWidth: number,\n viewportHeight: number,\n padding: number,\n): number {\n return Math.min(\n 1,\n ZOOM_MAX_SCALE,\n (viewportWidth - padding * 2) / contentWidth,\n (viewportHeight - padding * 2) / contentHeight,\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { GenreTreeNode } from \"./types\";\nimport { resolveInfoPanelSide } from \"./info-panel-geometry\";\n\nexport interface NodeInfoPanelState {\n node: GenreTreeNode;\n side: \"left\";\n}\n\nexport interface UseNodeInfoPanelOptions {\n /** The consumer-controlled selection (see GenreTreeProps.selectedNodeId). A transition from an\n * id to `null`/`undefined` closes the panel without firing `onSelectedNodeChange`, since the\n * consumer initiated it. */\n selectedNodeId?: string | null;\n /** See GenreTreeProps.onSelectedNodeChange. */\n onSelectedNodeChange?: (node: GenreTreeNode | null) => void;\n}\n\nexport interface UseNodeInfoPanelResult {\n panel: NodeInfoPanelState | null;\n /** Opens (or updates, if already open) the panel for `node`. No-ops if `element` or `viewport`\n * is missing (e.g. not yet mounted in jsdom). Notifies `onSelectedNodeChange` when the shown node\n * changes, unless `notify` is false — pass that when syncing to the consumer's `selectedNodeId`. */\n showNodeInfo: (\n node: GenreTreeNode,\n element: Element | null | undefined,\n viewport: Element | null | undefined,\n options?: { notify?: boolean },\n ) => void;\n closeNodeInfo: () => void;\n}\n\n/** Owns the \"which node's info panel is open, and on which side\" state for one renderer instance\n * (see GenreTree.tsx and the wheel renderers' top-level components) — not global, not lifted to\n * the consumer, mirroring how usePanZoom is instantiated once per viewport. Every open/close goes\n * through here, so this is the one place that notifies the consumer of selection changes. */\nexport function useNodeInfoPanel({\n selectedNodeId,\n onSelectedNodeChange,\n}: UseNodeInfoPanelOptions = {}): UseNodeInfoPanelResult {\n const [panel, setPanel] = useState<NodeInfoPanelState | null>(null);\n const [prevSelectedNodeId, setPrevSelectedNodeId] = useState(selectedNodeId);\n if (selectedNodeId !== prevSelectedNodeId) {\n setPrevSelectedNodeId(selectedNodeId);\n if (prevSelectedNodeId != null && selectedNodeId == null) setPanel(null);\n }\n\n // Also written synchronously in show/close so two calls in one tick compare against each other.\n const shownNodeIdRef = useRef<string | null>(null);\n const onSelectedNodeChangeRef = useRef(onSelectedNodeChange);\n useEffect(() => {\n shownNodeIdRef.current = panel?.node.id ?? null;\n onSelectedNodeChangeRef.current = onSelectedNodeChange;\n });\n\n const showNodeInfo = useCallback(\n (\n node: GenreTreeNode,\n element: Element | null | undefined,\n viewport: Element | null | undefined,\n { notify = true }: { notify?: boolean } = {},\n ) => {\n if (!element || !viewport) return;\n setPanel({ node, side: resolveInfoPanelSide() });\n const changed = node.id !== shownNodeIdRef.current;\n shownNodeIdRef.current = node.id;\n if (changed && notify) onSelectedNodeChangeRef.current?.(node);\n },\n [],\n );\n\n const closeNodeInfo = useCallback(() => {\n setPanel(null);\n shownNodeIdRef.current = null;\n onSelectedNodeChangeRef.current?.(null);\n }, []);\n\n return { panel, showNodeInfo, closeNodeInfo };\n}\n","import { INFO_PANEL_SHEET_MAX_WIDTH, INFO_PANEL_SHEET_PEEK_FRACTION } from \"./constants\";\n\n/** The part of the viewport an overlay covers, as `centerOnElement`'s \"obscured\" argument\n * (use-pan-zoom.ts). */\nexport type ObscuredArea = { side: \"left\" | \"right\"; width: number } | { side: \"bottom\"; height: number };\n\n/** The info panel always renders on the left side of the viewport. */\nexport function resolveInfoPanelSide(): \"left\" {\n return \"left\";\n}\n\n/** Packages the panel's footprint as the `centerOnElement` \"obscured\" argument (use-pan-zoom.ts) so\n * a node click can center the node within the space that remains visible once the panel covers\n * the left side — or, in a narrow container where it docks as a bottom sheet, its peek height. */\nexport function resolveInfoPanelObscuredArea(\n element: Element | null | undefined,\n viewport: Element | null | undefined,\n panelWidth: number,\n): ObscuredArea | null {\n if (!element || !viewport) return null;\n if (viewport.clientWidth > 0 && viewport.clientWidth <= INFO_PANEL_SHEET_MAX_WIDTH) {\n return { side: \"bottom\", height: viewport.clientHeight * INFO_PANEL_SHEET_PEEK_FRACTION };\n }\n return { width: panelWidth, side: resolveInfoPanelSide() };\n}\n","import { useMemo } from \"react\";\n\nimport { buildTreeIndex, TreeIndex } from \"./tree-index\";\nimport { GenreTreeNode } from \"./types\";\n\n/** `buildTreeIndex(nodes)`, rebuilt only when `nodes`' identity changes. */\nexport function useTreeIndex(nodes: readonly GenreTreeNode[]): TreeIndex {\n return useMemo(() => buildTreeIndex(nodes), [nodes]);\n}\n","import { GenreTreeNode } from \"./types\";\n\nexport interface TreeIndex {\n byId: ReadonlyMap<string, GenreTreeNode>;\n /** Direct children per parent id, in `nodes` order. */\n childrenByParentId: ReadonlyMap<string, readonly GenreTreeNode[]>;\n /** Each node's top-level ancestor id (a node with `parentId === null`), or `null` when its\n * `parentId` chain terminates on an id absent from `nodes` (dangling reference). */\n rootIdById: ReadonlyMap<string, string | null>;\n /** `id`'s ancestors, root-first, excluding `id` itself — stops at a dangling `parentId`. Empty\n * for a root or an unknown id. */\n ancestorsOf: (id: string) => GenreTreeNode[];\n}\n\n/** One O(n) pass over `nodes` building every lookup the renderers need, so per-node queries\n * (root id, children, ancestors, by-id) stay O(1)/O(depth) instead of rescanning `nodes`. */\nexport function buildTreeIndex(nodes: readonly GenreTreeNode[]): TreeIndex {\n const byId = new Map(nodes.map((node) => [node.id, node]));\n\n const childrenByParentId = new Map<string, GenreTreeNode[]>();\n for (const node of nodes) {\n if (node.parentId === null) continue;\n const siblings = childrenByParentId.get(node.parentId);\n if (siblings) siblings.push(node);\n else childrenByParentId.set(node.parentId, [node]);\n }\n\n const rootIdById = new Map<string, string | null>();\n const rootIdOf = (id: string): string | null => {\n if (rootIdById.has(id)) return rootIdById.get(id)!;\n const node = byId.get(id);\n if (!node) return null;\n const rootId = node.parentId === null ? node.id : rootIdOf(node.parentId);\n rootIdById.set(id, rootId);\n return rootId;\n };\n for (const node of nodes) rootIdOf(node.id);\n\n const ancestorsOf = (id: string): GenreTreeNode[] => {\n const ancestors: GenreTreeNode[] = [];\n let parentId = byId.get(id)?.parentId ?? null;\n while (parentId !== null) {\n const parent = byId.get(parentId);\n if (!parent) break;\n ancestors.push(parent);\n parentId = parent.parentId;\n }\n return ancestors.reverse();\n };\n\n return { byId, childrenByParentId, rootIdById, ancestorsOf };\n}\n","import { PointerEvent, ReactNode, useEffect, useRef, useState } from \"react\";\nimport { MdArrowBack, MdClose, MdZoomIn } from \"react-icons/md\";\n\nimport { getItemCountRange, INFO_PANEL_SHEET_DRAG_THRESHOLD_PX, ItemCountRange, logarithmicPosition } from \"./constants\";\nimport { InfoPanelExpandButton } from \"./InfoPanelExpandButton\";\nimport { InfoPanelSection } from \"./InfoPanelSection\";\nimport { NodeToolbar, NodeToolbarProps } from \"./NodeToolbar\";\nimport { GenreTreeNode, InfoPanelBody, InfoPanelSections } from \"./types\";\n\n// Same log-scaled position as a tree card (scaled across the mini tree's own nodes), mapped onto\n// a range that stays legible in the 25.5rem panel instead of the tree's 35-120px card heights.\nconst MINI_NODE_MIN_DIAMETER = 14;\nconst MINI_NODE_MAX_DIAMETER = 32;\nconst MINI_NODE_MIN_FONT_SIZE = 10;\nconst MINI_NODE_MAX_FONT_SIZE = 14;\n\n/** A direct child of the panel's node, styled to match exactly how that child renders as its own\n * `gtv-node-rect` out in the tree (solid vs. tinted fill, light vs. dark text) — computed by the\n * caller, since that styling depends on renderer-specific state (e.g. `hideRoot`, pop/core sector)\n * this component has no visibility into. */\nexport interface InfoPanelChild {\n node: GenreTreeNode;\n fill: string;\n textColor: string;\n}\n\nexport interface InfoPanelProps {\n node: GenreTreeNode;\n /** Fill and text color matching how `node` itself renders as its own `gtv-node-rect` out in the\n * tree — computed by the caller for the same reason as `InfoPanelChild`'s (see above). */\n fill: string;\n textColor: string;\n /** `node`'s own parent, styled the same way as `InfoPanelChild` — null for a root, which has\n * none. */\n parentNode: InfoPanelChild | null;\n childNodes: InfoPanelChild[];\n /** `node`'s ancestors root-first, including its immediate parent. Empty for a root. */\n ancestorNodes: InfoPanelChild[];\n side: \"left\" | \"right\";\n onClose: () => void;\n /** Fired when the parent chip or a child chip is clicked, with that node's id — the caller\n * navigates the panel (and the tree's own selection/centering) to it. */\n onSelectNode: (nodeId: string) => void;\n /** Reselects the previously selected node; the button is hidden when omitted. */\n onPrevious?: () => void;\n previousLabel?: string;\n /** See GenreTreeProps.hideInfoPanelClose. */\n hideClose?: boolean;\n /** See GenreTreeProps.hideInfoPanelTitle. */\n hideTitle?: boolean;\n /** See GenreTreeProps.itemCountLabel. */\n itemCountLabel: string;\n /** See GenreTreeProps.renderInfoPanelBody. */\n renderInfoPanelBody?: (node: GenreTreeNode, sections: InfoPanelSections) => InfoPanelBody;\n /** When set, shows a \"Zoom in\" action that fires it. */\n onZoomIn?: () => void;\n /** When set, renders the node's play/add-child/rename/reparent/delete actions below the\n * header. Omitted when the toolbar is disabled (see GenreTreeProps.showToolbar). */\n actions?: Omit<NodeToolbarProps, \"node\" | \"className\">;\n /** See GenreTreeProps.readOnly. */\n readOnly?: boolean;\n /** See GenreTreeProps.renderDangerZone. */\n renderDangerZone?: (node: GenreTreeNode) => ReactNode;\n /** See GenreTreeProps.expanded. */\n expanded?: boolean;\n /** See GenreTreeProps.onExpandedChange. */\n onExpandedChange?: (on: boolean) => void;\n /** See GenreTreeProps.collapsed. */\n collapsed?: boolean;\n /** See GenreTreeProps.hideExpandButton. */\n hideExpandButton?: boolean;\n}\n\nexport function isEscapeOwnedElsewhere(event: KeyboardEvent): boolean {\n if (event.defaultPrevented) return true;\n const target = event.target;\n if (target instanceof Element && target.closest(\"input, textarea, select, [contenteditable]:not([contenteditable=false])\")) {\n return true;\n }\n return document.querySelector('dialog[open], [role=\"dialog\"][aria-modal=\"true\"], [role=\"alertdialog\"]') !== null;\n}\n\n/** Displays a clicked node's own fields, its parent, and its direct children — no fetching, no\n * mutation, purely presentational (see ARCHITECTURE.md). Anchored to `side`\n * (see use-node-info-panel.ts/info-panel-geometry.ts for how that's decided) by the caller's own\n * layout, not by this component. */\nexport function InfoPanel({\n node,\n fill,\n textColor,\n parentNode,\n childNodes,\n ancestorNodes,\n side,\n onClose,\n onSelectNode,\n onPrevious,\n previousLabel,\n renderInfoPanelBody,\n hideClose = false,\n hideTitle = false,\n itemCountLabel,\n onZoomIn,\n actions,\n readOnly = false,\n renderDangerZone,\n expanded = false,\n onExpandedChange,\n collapsed = false,\n hideExpandButton = false,\n}: InfoPanelProps) {\n const panelRef = useRef<HTMLDivElement>(null);\n // Only styled in a narrow container, where the panel docks to the bottom as a sheet: peek covers\n // its lower part, full rises to the panel's usual top.\n const [sheetFull, setSheetFull] = useState(false);\n const grabberDragRef = useRef<{ startY: number; dragged: boolean } | null>(null);\n const onGrabberPointerDown = (event: PointerEvent<HTMLButtonElement>) => {\n event.currentTarget.setPointerCapture?.(event.pointerId);\n grabberDragRef.current = { startY: event.clientY, dragged: false };\n };\n const onGrabberPointerUp = (event: PointerEvent<HTMLButtonElement>) => {\n const drag = grabberDragRef.current;\n if (!drag) return;\n const dy = event.clientY - drag.startY;\n if (Math.abs(dy) < INFO_PANEL_SHEET_DRAG_THRESHOLD_PX) return;\n drag.dragged = true;\n if (dy < 0) setSheetFull(true);\n else if (sheetFull) setSheetFull(false);\n else if (!hideClose) onClose();\n };\n const onGrabberClick = () => {\n const dragged = grabberDragRef.current?.dragged ?? false;\n grabberDragRef.current = null;\n if (!dragged) setSheetFull((full) => !full);\n };\n // `inert` is a boolean attribute in React 19 but an unknown string one in 18 (the peer range covers both).\n useEffect(() => {\n panelRef.current?.toggleAttribute(\"inert\", collapsed);\n }, [collapsed]);\n const miniTreeRange = getItemCountRange([\n ...(parentNode ? [parentNode.node] : []),\n node,\n ...childNodes.map((child) => child.node),\n ]);\n const ancestorRange = getItemCountRange(ancestorNodes.map((ancestor) => ancestor.node));\n const miniNodeSize = ({ itemCount }: GenreTreeNode, range: ItemCountRange) => {\n const t = logarithmicPosition(itemCount, range);\n return {\n diameter: MINI_NODE_MIN_DIAMETER + t * (MINI_NODE_MAX_DIAMETER - MINI_NODE_MIN_DIAMETER),\n fontSize: MINI_NODE_MIN_FONT_SIZE + t * (MINI_NODE_MAX_FONT_SIZE - MINI_NODE_MIN_FONT_SIZE),\n };\n };\n const miniNodeContent = (target: GenreTreeNode, background: string, range: ItemCountRange) => {\n const { diameter, fontSize } = miniNodeSize(target, range);\n return (\n <>\n <span className=\"gtv-info-panel-mini-dot\" style={{ background, width: diameter, height: diameter }} />\n <span className=\"gtv-info-panel-mini-label\" style={{ fontSize }}>\n {target.name}\n </span>\n </>\n );\n };\n const sortedChildNodes = [...childNodes].sort((a, b) => b.node.itemCount - a.node.itemCount);\n\n useEffect(() => {\n if (!expanded || !onExpandedChange) return;\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\" || isEscapeOwnedElsewhere(event)) return;\n onExpandedChange(false);\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [expanded, onExpandedChange]);\n\n const dangerZone = !readOnly && renderDangerZone && (\n <div className=\"gtv-info-panel-danger-zone\">{renderDangerZone(node)}</div>\n );\n\n const toolbar = actions ? <NodeToolbar key={node.id} node={node} className=\"gtv-info-panel-toolbar\" {...actions} /> : null;\n const ancestors =\n ancestorNodes.length > 0 ? (\n <InfoPanelSection title=\"Ancestors\">\n <ul className=\"gtv-info-panel-children-list gtv-info-panel-ancestors-list\">\n {ancestorNodes.map(({ node: ancestor, fill }) => (\n <li key={ancestor.id}>\n <button\n type=\"button\"\n className=\"gtv-info-panel-child\"\n title={ancestor.name}\n onClick={() => onSelectNode(ancestor.id)}\n >\n {miniNodeContent(ancestor, fill, ancestorRange)}\n </button>\n </li>\n ))}\n </ul>\n </InfoPanelSection>\n ) : null;\n const family = (\n <>\n <InfoPanelSection title=\"Family\">\n <div className=\"gtv-info-panel-mini-tree\">\n {parentNode && (\n <div className=\"gtv-info-panel-mini-tree-parent\">\n <button\n type=\"button\"\n className=\"gtv-info-panel-child\"\n title={parentNode.node.name}\n onClick={() => onSelectNode(parentNode.node.id)}\n >\n {miniNodeContent(parentNode.node, parentNode.fill, miniTreeRange)}\n </button>\n </div>\n )}\n <div\n className={`gtv-info-panel-mini-tree-node${childNodes.length === 0 ? \" gtv-info-panel-mini-tree-node--leaf\" : \"\"}`}\n >\n <span\n className=\"gtv-info-panel-child gtv-info-panel-mini-tree-current\"\n title={node.name}\n >\n {miniNodeContent(node, fill, miniTreeRange)}\n </span>\n </div>\n <div className=\"gtv-info-panel-mini-tree-children\">\n <span className=\"gtv-info-panel-children-title\">{childNodes.length} {childNodes.length <= 1 ? \"child\" : \"children\"}</span>\n {childNodes.length > 0 && (\n <ul className=\"gtv-info-panel-children-list\">\n {sortedChildNodes.map(({ node: child, fill }) => (\n <li key={child.id}>\n <button\n type=\"button\"\n className=\"gtv-info-panel-child\"\n title={child.name}\n onClick={() => onSelectNode(child.id)}\n >\n {miniNodeContent(child, fill, miniTreeRange)}\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n </InfoPanelSection>\n </>\n );\n const itemCount = (\n <dl className=\"gtv-info-panel-fields\">\n <dt>{itemCountLabel}</dt>\n <dd>{node.itemCount}</dd>\n </dl>\n );\n const { primary, aside } = renderInfoPanelBody\n ? renderInfoPanelBody(node, { toolbar, ancestors, family, itemCount })\n : { primary: <>{toolbar}{ancestors}{family}{itemCount}</> };\n const hasAside = aside !== undefined && aside !== null && aside !== false;\n\n return (\n <div\n className={`gtv-info-panel gtv-info-panel--sheet${sheetFull ? \" gtv-info-panel--sheet-full\" : \"\"} gtv-info-panel--${side}${expanded ? \" gtv-info-panel--expanded\" : \"\"}${expanded && !hasAside ? \" gtv-info-panel--expanded-solo\" : \"\"}${collapsed ? \" gtv-info-panel--collapsed\" : \"\"}`}\n ref={panelRef}\n >\n <button\n type=\"button\"\n className=\"gtv-info-panel-grabber\"\n aria-label={sheetFull ? \"Lower panel\" : \"Raise panel\"}\n aria-expanded={sheetFull}\n onPointerDown={onGrabberPointerDown}\n onPointerUp={onGrabberPointerUp}\n onClick={onGrabberClick}\n />\n <div\n className=\"gtv-info-panel-header\"\n style={{ background: fill, color: textColor }}\n >\n {onPrevious && (\n <button\n type=\"button\"\n className=\"gtv-info-panel-previous\"\n style={{ color: textColor }}\n onClick={onPrevious}\n aria-label={previousLabel ? `Previous: ${previousLabel}` : \"Previous\"}\n title={previousLabel}\n >\n <MdArrowBack className=\"gtv-icon\" size={16} />\n </button>\n )}\n <span\n className={`gtv-info-panel-title${hideTitle ? \" gtv-info-panel-title--hidden\" : \"\"}`}\n style={{ color: textColor }}\n >\n {node.name}\n {node.umbrella && <span className=\"gtv-info-panel-umbrella-badge\">umbrella</span>}\n </span>\n {onZoomIn && (\n <button\n type=\"button\"\n className=\"gtv-info-panel-zoom-in\"\n style={{ color: textColor }}\n onClick={onZoomIn}\n aria-label=\"Zoom in\"\n title=\"Zoom in\"\n >\n <MdZoomIn className=\"gtv-icon\" size={16} />\n </button>\n )}\n {onExpandedChange && !hideExpandButton && (\n <InfoPanelExpandButton expanded={expanded} onExpandedChange={onExpandedChange} style={{ color: textColor }} />\n )}\n {!hideClose && (\n <button\n type=\"button\"\n className=\"gtv-info-panel-close\"\n style={{ color: textColor }}\n onClick={onClose}\n aria-label=\"Close\"\n >\n <MdClose className=\"gtv-icon\" size={16} />\n </button>\n )}\n </div>\n <div className=\"gtv-info-panel-body\">\n <div className=\"gtv-info-panel-primary\">\n {primary}\n {expanded && !hasAside && dangerZone}\n </div>\n {hasAside && (\n <div className=\"gtv-info-panel-aside\">\n {aside}\n {expanded && dangerZone}\n </div>\n )}\n </div>\n {!expanded && dangerZone}\n </div>\n );\n}\n","import { CSSProperties } from \"react\";\nimport { MdCloseFullscreen, MdOpenInFull } from \"react-icons/md\";\n\ninterface InfoPanelExpandButtonProps {\n expanded: boolean;\n onExpandedChange: (on: boolean) => void;\n style?: CSSProperties;\n}\n\nexport function InfoPanelExpandButton({ expanded, onExpandedChange, style }: InfoPanelExpandButtonProps) {\n return (\n <button\n type=\"button\"\n className=\"gtv-info-panel-close\"\n style={style}\n onClick={() => onExpandedChange(!expanded)}\n aria-label={expanded ? \"Collapse panel\" : \"Expand panel\"}\n >\n {expanded ? <MdCloseFullscreen className=\"gtv-icon\" size={16} /> : <MdOpenInFull className=\"gtv-icon\" size={16} />}\n </button>\n );\n}\n","import { ReactNode, useState } from \"react\";\nimport { MdExpandMore } from \"react-icons/md\";\n\ninterface InfoPanelSectionProps {\n title: string;\n children: ReactNode;\n}\n\n/** A titled InfoPanel section whose body collapses/expands from a chevron toggle in its header. */\nexport function InfoPanelSection({ title, children }: InfoPanelSectionProps) {\n const [open, setOpen] = useState(true);\n return (\n <div className=\"gtv-info-panel-children\">\n <button\n type=\"button\"\n className=\"gtv-info-panel-section-toggle\"\n aria-expanded={open}\n onClick={() => setOpen((isOpen) => !isOpen)}\n >\n <span className=\"gtv-info-panel-children-title\">{title}</span>\n <MdExpandMore className=\"gtv-icon gtv-info-panel-section-chevron\" size={16} />\n </button>\n {open && children}\n </div>\n );\n}\n","\"use client\";\n\nimport { useState } from \"react\";\nimport { MdMoreVert, MdModeEdit } from \"react-icons/md\";\nimport { FaPlus, FaTrashAlt, FaPlay, FaPause, FaSpinner } from \"react-icons/fa\";\nimport { PiGraphFill } from \"react-icons/pi\";\n\nimport { GenreTreeAction, GenreTreeNode, GenreTreePlayState } from \"./types\";\n\nexport interface NodeToolbarProps {\n node: GenreTreeNode;\n playingNodeId?: string | null;\n playState?: GenreTreePlayState;\n onPlayPause?: (nodeId: string) => void;\n onAddChild?: (parentId: string) => void;\n onRenameRequest?: (node: GenreTreeNode) => void;\n onDeleteRequest?: (node: GenreTreeNode) => void;\n onReparentRequest?: (node: GenreTreeNode) => void;\n additionalActions?: (node: GenreTreeNode) => GenreTreeAction[];\n className?: string;\n}\n\n/** The play/add-child/rename/reparent/delete/extra actions for a node, rendered in the info panel. */\nexport function NodeToolbar({\n node,\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n className,\n}: NodeToolbarProps) {\n const [menuOpen, setMenuOpen] = useState(false);\n const isActionable = node.actionable !== false;\n const extraActions = additionalActions?.(node) ?? [];\n const primaryExtraActions = extraActions.filter((a) => a.placement === \"primary\");\n const overflowExtraActions = extraActions.filter((a) => a.placement !== \"primary\");\n const isPlayable = node.itemCount > 0;\n const isThisPlaying = playingNodeId === node.id;\n const playLabel = isThisPlaying && playState === \"playing\" ? \"Pause\" : isThisPlaying && playState === \"loading\" ? \"Loading...\" : \"Play\";\n const playIcon =\n isThisPlaying && playState === \"playing\" ? (\n <FaPause className=\"gtv-icon\" size=\"0.6em\" />\n ) : isThisPlaying && playState === \"loading\" ? (\n <FaSpinner className=\"gtv-icon gtv-icon--spin\" size=\"0.6em\" />\n ) : (\n <FaPlay className=\"gtv-icon\" size=\"0.6em\" />\n );\n\n return (\n <div className={[\"gtv-toolbar\", className].filter(Boolean).join(\" \")}>\n <button\n type=\"button\"\n className={`gtv-toolbar-btn${!isPlayable ? \" gtv-toolbar-btn--disabled\" : \"\"}`}\n title={playLabel}\n aria-label={playLabel}\n disabled={!isPlayable}\n onClick={() => onPlayPause?.(node.id)}\n >\n {playIcon}\n </button>\n\n {isActionable && (\n <>\n <button\n type=\"button\"\n className=\"gtv-toolbar-btn\"\n title=\"Add sub-genre\"\n aria-label=\"Add sub-genre\"\n onClick={() => onAddChild?.(node.id)}\n >\n <FaPlus className=\"gtv-icon\" size=\"0.6em\" />\n </button>\n {primaryExtraActions.map((action) => {\n const enabled = action.enabled ? action.enabled(node) : true;\n const label = action.label(node);\n return (\n <button\n key={action.key}\n type=\"button\"\n className={`gtv-toolbar-btn${!enabled ? \" gtv-toolbar-btn--disabled\" : \"\"}`}\n title={label}\n aria-label={label}\n disabled={!enabled}\n onClick={(event) => action.onClick(event, node)}\n >\n {action.icon(node)}\n </button>\n );\n })}\n <div className=\"gtv-toolbar-overflow\">\n <button\n type=\"button\"\n className=\"gtv-toolbar-btn\"\n title=\"More actions\"\n aria-label=\"More actions\"\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n onClick={() => setMenuOpen((open) => !open)}\n >\n <MdMoreVert size=\"0.6em\" />\n </button>\n {menuOpen && (\n <div className=\"gtv-menu-card gtv-toolbar-overflow-menu\">\n <button\n type=\"button\"\n className=\"gtv-menu-row\"\n onClick={() => {\n setMenuOpen(false);\n onRenameRequest?.(node);\n }}\n >\n <span className=\"gtv-menu-row-icon\">\n <MdModeEdit className=\"gtv-icon\" size={13} />\n </span>\n <span className=\"gtv-menu-row-label\">Rename</span>\n </button>\n <button\n type=\"button\"\n className=\"gtv-menu-row\"\n onClick={() => {\n setMenuOpen(false);\n onReparentRequest?.(node);\n }}\n >\n <span className=\"gtv-menu-row-icon\">\n <PiGraphFill className=\"gtv-icon\" size={13} />\n </span>\n <span className=\"gtv-menu-row-label\">Change parent</span>\n </button>\n {overflowExtraActions.map((action) => {\n const enabled = action.enabled ? action.enabled(node) : true;\n return (\n <button\n key={action.key}\n type=\"button\"\n className={`gtv-menu-row${action.danger ? \" gtv-menu-row--danger\" : \"\"}${!enabled ? \" gtv-menu-row--disabled\" : \"\"}`}\n disabled={!enabled}\n onClick={(event) => {\n setMenuOpen(false);\n action.onClick(event, node);\n }}\n >\n <span className=\"gtv-menu-row-icon\">{action.icon(node)}</span>\n <span className=\"gtv-menu-row-label\">{action.label(node)}</span>\n </button>\n );\n })}\n <div className=\"gtv-menu-divider\" />\n <button\n type=\"button\"\n className=\"gtv-menu-row gtv-menu-row--danger\"\n onClick={() => {\n setMenuOpen(false);\n onDeleteRequest?.(node);\n }}\n >\n <span className=\"gtv-menu-row-icon\">\n <FaTrashAlt className=\"gtv-icon\" size={13} />\n </span>\n <span className=\"gtv-menu-row-label\">Delete</span>\n </button>\n </div>\n )}\n </div>\n </>\n )}\n </div>\n );\n}\n","import { useEffect, useRef } from \"react\";\nimport { MdClose } from \"react-icons/md\";\n\nimport { isEscapeOwnedElsewhere } from \"./InfoPanel\";\nimport { InfoPanelExpandButton } from \"./InfoPanelExpandButton\";\nimport { GenreTreeOverlayPanel } from \"./types\";\n\n/** Consumer-supplied panel reusing the info panel's classes, so it covers exactly the same area —\n * see GenreTreeProps.overlayPanel. */\nexport function OverlayPanel({\n title,\n content,\n onClose,\n expanded: controlledExpanded,\n onExpandedChange,\n extraContent,\n collapsed = false,\n hideExpandButton = false,\n hideTitle = false,\n hideClose = false,\n}: GenreTreeOverlayPanel) {\n const panelRef = useRef<HTMLDivElement>(null);\n const expandable = !!onExpandedChange;\n const expanded = expandable ? !!controlledExpanded : true;\n\n // `inert` is a boolean attribute in React 19 but an unknown string one in 18 (the peer range covers both).\n useEffect(() => {\n panelRef.current?.toggleAttribute(\"inert\", collapsed);\n }, [collapsed]);\n\n useEffect(() => {\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\" || isEscapeOwnedElsewhere(event)) return;\n if (expanded && onExpandedChange) onExpandedChange(false);\n else onClose();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [expanded, onExpandedChange, onClose]);\n\n const className = [\n \"gtv-info-panel\",\n expandable ? \"gtv-info-panel--left\" : \"gtv-info-panel--right gtv-info-panel--overlay\",\n expanded && \"gtv-info-panel--expanded\",\n expanded && !extraContent && \"gtv-info-panel--expanded-solo\",\n collapsed && \"gtv-info-panel--collapsed\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <div className={className} ref={panelRef}>\n <div className=\"gtv-info-panel-header\">\n <span className={`gtv-info-panel-title${hideTitle ? \" gtv-info-panel-title--hidden\" : \"\"}`}>{title}</span>\n {onExpandedChange && !hideExpandButton && (\n <InfoPanelExpandButton expanded={expanded} onExpandedChange={onExpandedChange} />\n )}\n {!hideClose && (\n <button type=\"button\" className=\"gtv-info-panel-close\" onClick={onClose} aria-label=\"Close\">\n <MdClose className=\"gtv-icon\" size={16} />\n </button>\n )}\n </div>\n <div className=\"gtv-info-panel-body\">\n <div className=\"gtv-info-panel-aside gtv-overlay-content\">{content}</div>\n {extraContent && <div className=\"gtv-info-panel-primary gtv-overlay-content\">{extraContent}</div>}\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as d3 from \"d3\";\n\nimport { GenreTree } from \"./GenreTree\";\nimport { calculateRootAnchorClearance } from \"./NodeHelper\";\nimport { computeAncestorChain, findRootId, groupNodesByRoot } from \"./root-grouping\";\nimport {\n buildWheelSectorGradient,\n calculateWheelRadius,\n computeRotationForSelection,\n getChipAngle,\n getWheelDividerAngle,\n} from \"./wheel-geometry\";\nimport { FloatingControls } from \"./FloatingControls\";\nimport { usePanZoom } from \"./use-pan-zoom\";\nimport { queryTreeContentElements } from \"./zoom-pan\";\nimport { useNodeInfoPanel } from \"./use-node-info-panel\";\nimport { resolveInfoPanelObscuredArea } from \"./info-panel-geometry\";\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\nimport { GenreTreeNode, GenreTreeProps, TreeOrientation } from \"./types\";\nimport {\n ACCENT_TEXT_COLOR,\n calculateNodeDimensions,\n calculateNodeFontSize,\n getGenreTreeColor,\n getItemCountRange,\n hexToRgba,\n INFO_PANEL_WIDTH,\n MAX_NODE_HEIGHT,\n MAX_NODE_WIDTH,\n PER_TREE_ACCENT_DOT,\n ROOT_SECTOR_FILL_OPACITY,\n WHEEL_RADIUS,\n WHEEL_ROTATION_EASING,\n WHEEL_ROTATION_TRANSITION_MS,\n ZOOM_FOCUS_SCALE,\n} from \"./constants\";\n\nexport interface WheelCoreProps\n extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the selected root changes — on mount with the default selection, and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. Stays upright and\n * fixed regardless of the wheel's rotation. */\n centerLabel?: string;\n /** Which edge of the container the wheel hugs. \"bottom\" (GenreTreeWheel) lands the selected\n * chip top-center and grows the subtree upward; \"left\" (GenreTreeWheelRight) lands it\n * right-center and grows the subtree rightward. */\n direction: \"bottom\" | \"left\";\n /** When false, clicking a chip still selects its root and swaps in its subtree, but the wheel\n * itself stays at its current rotation instead of spinning the selected chip to the anchor.\n * Defaults to true. */\n allowWheelRotation?: boolean;\n}\n\n/**\n * Shared implementation behind both `GenreTreeWheel` (`direction=\"bottom\"`) and\n * `GenreTreeWheelRight` (`direction=\"left\"`): all root genres distributed evenly around a wheel\n * hugging one edge of its container. Clicking a chip rotates the wheel so that root lands at the\n * anchor for that edge, and swaps in its subtree — rendered as a single non-interactive\n * `<GenreTree>` growing away from that anchor. Only the selected root's subtree is ever mounted.\n */\nexport function WheelCore({\n nodes,\n className,\n onRootSelect,\n centerLabel,\n direction,\n allowWheelRotation = true,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n showToolbar = true,\n renderInfoPanelBody,\n onSelectedNodeChange,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n itemCountScaleMax,\n nodeCountForSizing,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n wheelZoom,\n defaultViewport,\n onViewportChange,\n controlsOrientation = \"vertical\",\n showZoomControls = \"auto\",\n floatingControlsLeading,\n busy,\n selectedNodeId,\n}: WheelCoreProps) {\n const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;\n const wheelSizeRange = useMemo(\n () => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),\n [nodes, itemCountScaleMax, fullNodeCountForSizing],\n );\n const treeOrientation: TreeOrientation =\n direction === \"left\" ? \"horizontal-anchored\" : \"vertical\";\n // CSS `rotate()` + `translateY(-radius)` convention: local angle 0°=top, 90°=right, 180°=bottom,\n // 270°=left — measured against the disc's own unrotated box, since its CSS `top`/`left`/`right`\n // position (not its `rotate()` transform, which only spins rendered content around the box's\n // center) is what's pinned to the container's anchor point. The bottom-hugging wheel's box has\n // its top-center pinned there (local angle 0). The left-hugging wheel's box is pinned by its\n // *right* edge (`right: 0` in the `--left` CSS below), i.e. its right-center point — local\n // angle 90 — so the selection has to land at 90, not at the \"left\" angle (270) the disc bulges\n // toward.\n const landingAngle = direction === \"left\" ? 90 : 0;\n const chipHalfExtentVar =\n direction === \"left\"\n ? \"--gtv-wheel-chip-half-width\"\n : \"--gtv-wheel-chip-half-height\";\n\n const groups = useMemo(() => groupNodesByRoot(nodes), [nodes]);\n const rootColorOf = (nodeId: string) => {\n const rootId = findRootId(nodeId, nodes) ?? nodeId;\n return getGenreTreeColor(nodes.find((n) => n.id === rootId)?.wikidataId);\n };\n\n // Reserve the current density-adjusted chip maximum for every neighboring pair. WHEEL_RADIUS is\n // a floor: the wheel never shrinks below its default, only grows to fit more/larger chips.\n const wheelRadius = useMemo(\n () => calculateWheelRadius(groups.length, wheelSizeRange.maxNodeWidth ?? MAX_NODE_WIDTH, WHEEL_RADIUS),\n [groups.length, wheelSizeRange.maxNodeWidth],\n );\n const wheelViewportHeight = wheelRadius * 2 + (wheelSizeRange.maxNodeHeight ?? MAX_NODE_HEIGHT) / 2;\n\n const rootItemCountById = useMemo(\n () => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),\n [groups],\n );\n const [selectedRootId, setSelectedRootId] = useState<string | null>(\n groups[0]?.root.id ?? null,\n );\n // The default selection (chip index 0) must start already rotated to the anchor, not just at\n // 0deg — 0 only happens to be correct for direction=\"bottom\", where landingAngle (0) coincides\n // with chip 0's own raw angle (getChipAngle(0, n) is always 0). For direction=\"left\"\n // (landingAngle 270) those differ, so the initial rotation has to be computed the same way a\n // click's rotation is.\n const [rotationDeg, setRotationDeg] = useState(() =>\n groups.length > 0\n ? computeRotationForSelection(\n 0,\n getChipAngle(0, groups.length),\n landingAngle,\n )\n : 0,\n );\n const viewportRef = useRef<HTMLDivElement>(null);\n // One shared pan/zoom transform, applied to the stage below that anchors both the tree and the\n // wheel to the same point — so panning/zooming moves them together with no JS sync required.\n const stageRef = useRef<HTMLDivElement>(null);\n const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });\n const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });\n // Fit-to-frame targets: the circle (not .gtv-wheel, which rotates and would inflate its own\n // axis-aligned bounding box) and the tree anchor (only mounted once a root is selected).\n const wheelCircleRef = useRef<HTMLDivElement>(null);\n const treeAnchorRef = useRef<HTMLDivElement>(null);\n\n // Falls back to the first root without writing state back when the explicitly selected root\n // disappears from `nodes` — avoids a setState-in-effect cascading render for derived state.\n const effectiveRootId = groups.some(\n (group) => group.root.id === selectedRootId,\n )\n ? selectedRootId\n : (groups[0]?.root.id ?? null);\n\n // Read via a ref rather than depending on `onRootSelect` directly — consumers commonly pass an\n // inline callback, which would otherwise re-fire this effect (and any state it sets) every render.\n const onRootSelectRef = useRef(onRootSelect);\n useEffect(() => {\n onRootSelectRef.current = onRootSelect;\n });\n\n useEffect(() => {\n if (effectiveRootId) onRootSelectRef.current?.(effectiveRootId);\n }, [effectiveRootId]);\n\n // Starts the view fit to the wheel + selected subtree instead of at scale 1 / pan (0, 0) —\n // guarded so it only fires once the tree anchor has actually rendered content, and never again\n // afterward so it doesn't fight the user's own pan/zoom on later selections.\n const hasInitialFitRef = useRef(false);\n useEffect(() => {\n if (hasInitialFitRef.current) return;\n const elements = [\n wheelCircleRef.current,\n ...queryTreeContentElements(treeAnchorRef.current),\n ];\n if (!elements.some(Boolean)) return;\n hasInitialFitRef.current = true;\n panZoom.fitToFrame(elements, { silent: true });\n });\n\n const selectedGroup =\n groups.find((group) => group.root.id === effectiveRootId) ?? null;\n\n // Root chip size is proportional to itemCount relative to the other roots on the wheel, not\n // an absolute scale — the root with the fewest items always renders at MIN size and the one\n // with the most always at MAX, regardless of the actual counts involved.\n const rootItemCountRange = useMemo(\n () =>\n getItemCountRange(\n groups.map((group) => ({\n itemCount: rootItemCountById.get(group.root.id)!,\n })),\n { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing },\n ),\n [groups, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing],\n );\n\n // The selected chip is centered on its wheel anchor point (translate(-50%, -50%)) so it stays\n // centered on the circle like every other chip, but the tree's root lands with its near edge\n // exactly at that same anchor point (see .gtv-wheel-tree-anchor). Left alone, that mismatch makes\n // the chip overlap the tree's near edge — offsetting the anchor by the chip's own half-extent\n // along the growth axis clears it, so the root->depth1 gap reads the same as any other\n // consecutive-depth gap. See calculateRootAnchorClearance's doc comment (and\n // GenreTreeWheelRadialBase for the equivalent radial-wheel computation) for why that offset needs\n // both the hidden root's own local half-extent and the visible chip's cross-root display\n // half-extent, not just one or the other.\n const rootChipHalfExtent = selectedGroup\n ? calculateRootAnchorClearance(\n d3,\n selectedGroup.nodes,\n rootItemCountById.get(selectedGroup.root.id)!,\n rootItemCountRange,\n direction === \"left\" ? \"WIDTH\" : \"HEIGHT\",\n { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing },\n )\n : 0;\n\n // Colors every root's chip index (i.e. getChipAngle(i, n)'s ordering) matches groups' own order,\n // so each color stop lines up with the sector buildWheelSectorGradient carves out for it.\n const sectorFillGradient = useMemo(\n () =>\n buildWheelSectorGradient(\n groups.map((group) =>\n hexToRgba(getGenreTreeColor(group.root.wikidataId), ROOT_SECTOR_FILL_OPACITY),\n ),\n ),\n [groups],\n );\n\n const handleChipClick = (rootId: string, angle: number) => {\n setSelectedRootId(rootId);\n if (allowWheelRotation) {\n setRotationDeg((current) =>\n computeRotationForSelection(current, angle, landingAngle),\n );\n }\n };\n\n // An externally-controlled selection (e.g. from a search result) may belong to a root other than\n // the one currently mounted — swap it in the same way a chip click would before trying to locate\n // the node's DOM element below. Adjusted during render rather than in an effect so the swap is\n // part of the same commit (see React's \"adjusting state when a prop changes\"), and only on an\n // actual change of the prop so a later chip click isn't undone.\n const [appliedSelectedNodeId, setAppliedSelectedNodeId] =\n useState(selectedNodeId);\n if (selectedNodeId !== appliedSelectedNodeId) {\n setAppliedSelectedNodeId(selectedNodeId);\n const owningGroupIndex = selectedNodeId\n ? groups.findIndex((group) =>\n group.nodes.some((node) => node.id === selectedNodeId),\n )\n : -1;\n if (\n owningGroupIndex !== -1 &&\n groups[owningGroupIndex].root.id !== effectiveRootId\n ) {\n handleChipClick(\n groups[owningGroupIndex].root.id,\n getChipAngle(owningGroupIndex, groups.length),\n );\n }\n }\n\n // Mirrors the onNodeClick/onSelectNode handling below: once the owning root's subtree is\n // mounted, center the viewport on the externally-selected node and open its info panel exactly\n // as a direct click would.\n useEffect(() => {\n if (!selectedNodeId || panel?.node.id === selectedNodeId) return;\n const targetNode = nodes.find((node) => node.id === selectedNodeId);\n const element = treeAnchorRef.current?.querySelector(\n `#group-${CSS.escape(selectedNodeId)}`,\n );\n if (!targetNode || !element) return;\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(targetNode, element, viewportRef.current, { notify: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps -- panZoom re-created on pan/zoom; guarded by the panel?.node.id check above\n }, [selectedNodeId, effectiveRootId, nodes, panel, showNodeInfo]);\n\n return (\n <div\n ref={viewportRef}\n aria-busy={busy}\n className={[\n \"gtv-wheel-container\",\n direction === \"left\" && \"gtv-wheel-container--left\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n \"--gtv-wheel-radius\": `${wheelRadius}px`,\n \"--gtv-wheel-viewport-height\": `${wheelViewportHeight}px`,\n \"--gtv-wheel-rotation-transition-ms\": `${WHEEL_ROTATION_TRANSITION_MS}ms`,\n \"--gtv-wheel-rotation-easing\": WHEEL_ROTATION_EASING,\n touchAction: panZoom.touchAction,\n } as React.CSSProperties\n }\n onPointerDown={panZoom.handlePointerDown}\n >\n {/* height: 100% (not just width) so this is the containing block .gtv-wheel-stage's own\n `bottom: var(--gtv-wheel-viewport-height)` measures against — otherwise, with only an\n absolutely-positioned child, this wrapper's height collapses to 0 and the stage's anchor\n point would land at the container's top instead of its bottom. The transform is purely\n visual and doesn't affect this box-model sizing. */}\n <div\n ref={stageRef}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n transform: panZoom.transform,\n transformOrigin: \"0 0\",\n }}\n >\n <div\n className=\"gtv-wheel-stage\"\n style={\n {\n [chipHalfExtentVar]: `${rootChipHalfExtent}px`,\n } as React.CSSProperties\n }\n >\n {selectedGroup && (\n <div className=\"gtv-wheel-tree-anchor\" ref={treeAnchorRef}>\n <GenreTree\n key={selectedGroup.root.id}\n nodes={selectedGroup.nodes}\n itemCountLabel={itemCountLabel}\n itemCountScaleMax={itemCountScaleMax}\n nodeCountForSizing={fullNodeCountForSizing}\n orientation={treeOrientation}\n hideRoot\n interactive={false}\n rootColor={getGenreTreeColor(selectedGroup.root.wikidataId)}\n playingNodeId={playingNodeId}\n playState={playState}\n reparentingNodeId={reparentingNodeId}\n onPlayPause={onPlayPause}\n onAddChild={onAddChild}\n onRenameRequest={onRenameRequest}\n onDeleteRequest={onDeleteRequest}\n onReparentRequest={onReparentRequest}\n onReparent={onReparent}\n onNodeClick={(data, event) => {\n const clickedElement = event.currentTarget as Element | null;\n panZoom.centerOnElement(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n }}\n additionalActions={additionalActions}\n showToolbar={showToolbar}\n selectedNodeId={panel?.node.id ?? null}\n />\n </div>\n )}\n\n <div className=\"gtv-wheel-circle\" ref={wheelCircleRef} />\n\n {centerLabel && (\n <div className=\"gtv-wheel-center-label\">{centerLabel}</div>\n )}\n\n <div\n className=\"gtv-wheel\"\n style={\n {\n \"--gtv-wheel-rotation\": `${rotationDeg}deg`,\n } as React.CSSProperties\n }\n >\n {sectorFillGradient && (\n <div\n className=\"gtv-wheel-sector-fill\"\n style={\n {\n \"--gtv-sector-fill-gradient\": sectorFillGradient,\n } as React.CSSProperties\n }\n />\n )}\n\n {groups.length > 1 &&\n groups.map((group, index) => (\n <div\n key={`divider-${group.root.id}`}\n className=\"gtv-wheel-divider\"\n style={\n {\n \"--gtv-divider-angle\": `${getWheelDividerAngle(index, groups.length)}deg`,\n } as React.CSSProperties\n }\n />\n ))}\n\n {groups.map((group, index) => {\n const angle = getChipAngle(index, groups.length);\n const selected = group.root.id === effectiveRootId;\n const chipColor = getGenreTreeColor(group.root.wikidataId);\n const rootItemCount = rootItemCountById.get(group.root.id)!;\n const dimensions = calculateNodeDimensions(\n rootItemCount,\n rootItemCountRange,\n );\n const fontSize = calculateNodeFontSize(\n rootItemCount,\n rootItemCountRange,\n );\n return (\n <div\n key={group.root.id}\n className=\"gtv-wheel-slot\"\n style={\n { \"--gtv-chip-angle\": `${angle}deg` } as React.CSSProperties\n }\n >\n <div className=\"gtv-wheel-chip-anchor\">\n <button\n type=\"button\"\n id={`group-${group.root.id}`}\n className={[\n \"gtv-wheel-chip\",\n selected && \"gtv-wheel-chip--selected\",\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n width: dimensions.WIDTH,\n // Height comes from --gtv-wheel-chip-base-height (see .gtv-wheel-chip in\n // styles.css), not an inline `height`/`minHeight` — an inline `height` always\n // wins the cascade over the CSS class's `height: calc(...)` hover override\n // (inline style beats any stylesheet rule), silently pinning the box and\n // breaking the hover growth again, same failure mode as before.\n \"--gtv-wheel-chip-base-height\": `${dimensions.HEIGHT}px`,\n \"--gtv-chip-color\": chipColor,\n } as React.CSSProperties\n }\n onClick={(event) => {\n panZoom.centerOnElement(\n event.currentTarget,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(event.currentTarget, viewportRef.current, INFO_PANEL_WIDTH),\n );\n handleChipClick(group.root.id, angle);\n if (!reparentingNodeId) {\n showNodeInfo(\n group.root,\n event.currentTarget,\n viewportRef.current,\n );\n onNodeClick?.(group.root, event.nativeEvent);\n }\n }}\n >\n {PER_TREE_ACCENT_DOT && (\n <span className=\"gtv-wheel-chip-dot\" />\n )}\n <span\n className=\"gtv-node-label gtv-node-label--root\"\n style={{ fontSize }}\n >\n {group.root.name}\n </span>\n </button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {panel && (\n <InfoPanel\n node={panel.node}\n fill={rootColorOf(panel.node.id)}\n textColor={ACCENT_TEXT_COLOR}\n parentNode={(() => {\n const parent = nodes.find((n) => n.id === panel.node.parentId);\n return parent\n ? {\n node: parent,\n fill: rootColorOf(parent.id),\n textColor: ACCENT_TEXT_COLOR,\n }\n : null;\n })()}\n childNodes={nodes\n .filter((n) => n.parentId === panel.node.id)\n .map((n) => {\n const rootColor = rootColorOf(n.id);\n // This wheel's inner subtree always renders via <GenreTree hideRoot ... />, so every\n // visible node — including every child listed here — gets a solid rootColor fill.\n return { node: n, fill: rootColor, textColor: ACCENT_TEXT_COLOR };\n })}\n ancestorNodes={computeAncestorChain(nodes, panel.node.id).map(\n (n): InfoPanelChild => ({\n node: n,\n fill: rootColorOf(n.id),\n textColor: ACCENT_TEXT_COLOR,\n }),\n )}\n side={panel.side}\n onClose={closeNodeInfo}\n onSelectNode={(id) => {\n const targetNode = nodes.find((n) => n.id === id)!;\n const element = viewportRef.current!.querySelector(\n `#group-${CSS.escape(id)}`,\n );\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(\n targetNode,\n element ?? viewportRef.current,\n viewportRef.current,\n );\n }}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n renderInfoPanelBody={renderInfoPanelBody}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n }\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n\n <FloatingControls\n panZoom={panZoom}\n orientation={controlsOrientation}\n zoomControls={showZoomControls}\n leading={floatingControlsLeading}\n busy={busy}\n onFit={() => panZoom.fitToFrame([\n wheelCircleRef.current,\n ...queryTreeContentElements(treeAnchorRef.current),\n ])}\n />\n </div>\n );\n}\n","import { GenreTreeNode } from \"./types\";\n\nexport interface GenreTreeRootGroup {\n root: GenreTreeNode;\n nodes: GenreTreeNode[];\n}\n\n/**\n * Walks `nodeId`'s `parentId` chain up through `nodes` to find its top-level ancestor (a node\n * with `parentId === null`). Returns `null` if the chain terminates on a `parentId` absent from\n * `nodes` (dangling reference) or `nodeId` itself isn't in `nodes`.\n */\nexport function findRootId(nodeId: string, nodes: GenreTreeNode[]): string | null {\n const nodeById = new Map(nodes.map((node) => [node.id, node]));\n\n const rootIdOf = (id: string): string | null => {\n const node = nodeById.get(id);\n return !node ? null : node.parentId === null ? node.id : rootIdOf(node.parentId);\n };\n\n return rootIdOf(nodeId);\n}\n\n/**\n * Groups a flat node list by top-level ancestor (a node with `parentId === null`), walking each\n * node's `parentId` chain up to find which root it belongs to. A node whose chain terminates on a\n * `parentId` absent from `nodes` (dangling reference) belongs to no group.\n */\nexport function groupNodesByRoot(nodes: GenreTreeNode[]): GenreTreeRootGroup[] {\n const nodeById = new Map(nodes.map((node) => [node.id, node]));\n const rootIdCache = new Map<string, string | null>();\n\n const rootIdOf = (nodeId: string): string | null => {\n if (rootIdCache.has(nodeId)) return rootIdCache.get(nodeId)!;\n const node = nodeById.get(nodeId);\n const rootId = !node ? null : node.parentId === null ? node.id : rootIdOf(node.parentId);\n rootIdCache.set(nodeId, rootId);\n return rootId;\n };\n\n const nodesByRootId = new Map<string, GenreTreeNode[]>();\n for (const node of nodes) {\n const rootId = rootIdOf(node.id);\n if (rootId === null) continue;\n const group = nodesByRootId.get(rootId);\n if (group) group.push(node);\n else nodesByRootId.set(rootId, [node]);\n }\n\n return nodes\n .filter((node) => node.parentId === null)\n .map((root) => ({ root, nodes: nodesByRootId.get(root.id)! }));\n}\n\n/**\n * Walks upward from `parentId` through `nodes`, collecting every ancestor above it (root-first) —\n * excludes `parentId`'s own node, since callers already show the immediate parent separately.\n * Stops if the chain terminates on a `parentId` absent from `nodes` (dangling reference).\n */\nexport function computeAncestorChain(nodes: GenreTreeNode[], parentId: string | null): GenreTreeNode[] {\n const ancestors: GenreTreeNode[] = [];\n let current = nodes.find((n) => n.id === parentId);\n while (current && current.parentId !== null) {\n const parent = nodes.find((n) => n.id === current!.parentId);\n if (!parent) break;\n ancestors.unshift(parent);\n current = parent;\n }\n return ancestors;\n}\n","/** The static angle (CSS `rotate()` convention: 0° = top, clockwise) of the `index`-th of\n * `totalCount` chips, evenly spaced around the full circle before any wheel rotation is\n * applied. Every chip stays reachable regardless of spacing — the whole circle is always\n * on screen and any chip can be brought to the top by rotating the wheel. */\nexport function getChipAngle(index: number, totalCount: number): number {\n return index * (360 / totalCount);\n}\n\n/** Angle (CSS `rotate()` convention) of the boundary between the `index`-th chip and its next\n * neighbor clockwise, for `totalCount` evenly-spaced chips — the midpoint of their spacing, used\n * to draw a radial divider separating the two. */\nexport function getWheelDividerAngle(index: number, totalCount: number): number {\n return getChipAngle(index, totalCount) + 180 / totalCount;\n}\n\n/** CSS `conic-gradient(...)` value tinting each of `totalCount` evenly-spaced chips' angular\n * span with its own color (see ROOT_SECTOR_FILL_OPACITY / hexToRgba in constants.ts for producing\n * a translucent entry) — `colors[i]` must correspond to the chip at `getChipAngle(i, totalCount)`.\n * Hard stops (not a smooth blend) at each getWheelDividerAngle boundary, so a sector's fill lines\n * up exactly with its bounding divider lines. A single static gradient works here (unlike the\n * radial wheel variants) because this wheel's chips are evenly spaced and the whole `.gtv-wheel`\n * box rotates as one unit — nesting this inside that box lets it rotate for free instead of\n * needing its own per-sector rotation. Empty string (no fill) below 2 colors, matching the \"no\n * dividers for 1 root\" rule. */\nexport function buildWheelSectorGradient(colors: string[]): string {\n const n = colors.length;\n if (n < 2) return \"\";\n const span = 360 / n;\n // Starts the gradient at chip 0's own leading boundary so the first hard stop lands exactly on\n // a divider instead of splitting chip 0's own sector across the gradient's 0deg seam.\n const startAngle = getWheelDividerAngle(n - 1, n);\n const stops = colors.map((color, i) => `${color} ${i * span}deg ${(i + 1) * span}deg`).join(\", \");\n return `conic-gradient(from ${startAngle}deg, ${stops})`;\n}\n\n/** Minimum wheel radius so adjacent chips — each up to `maxChipWidth` wide in the worst case —\n * don't overlap. The chord between two adjacent chip anchor points on the circle\n * (2 * radius * sin(pi / rootCount)) must be at least `maxChipWidth` plus a small gap; solving\n * for radius gives the minimum that keeps every pair of neighbors clear. Falls back to\n * `baseRadius` when that's already big enough, or when there's only one chip and so no neighbor\n * to overlap. */\nexport function calculateWheelRadius(rootCount: number, maxChipWidth: number, baseRadius: number): number {\n if (rootCount <= 1) return baseRadius;\n const CHIP_GAP = 16;\n const requiredRadius = (maxChipWidth + CHIP_GAP) / (2 * Math.sin(Math.PI / rootCount));\n return Math.max(baseRadius, requiredRadius);\n}\n\nfunction mod360(value: number): number {\n return ((value % 360) + 360) % 360;\n}\n\n/**\n * Computes the wheel rotation (degrees) that brings the chip at `targetAngle` to `landingAngle`\n * (default 0, the top — used by the bottom-hugging wheel; 270, the left, lands a chip at the\n * right — used by the left-hugging wheel), taking the shortest path from `currentRotationDeg`.\n * The wheel's own rotation composes with each chip's static angle, so bringing `targetAngle` to\n * `landingAngle` requires a rotation congruent to `landingAngle - targetAngle` (mod 360).\n *\n * The result is never renormalized into [0, 360) — it keeps accumulating so a CSS\n * `transition: transform` never has to jump across a 359°→0° wraparound.\n */\nexport function computeRotationForSelection(\n currentRotationDeg: number,\n targetAngle: number,\n landingAngle = 0,\n): number {\n const targetMod = mod360(landingAngle - targetAngle);\n const delta = targetMod - mod360(currentRotationDeg);\n const shortestDelta = delta - 360 * Math.round(delta / 360);\n return currentRotationDeg + shortestDelta;\n}\n","\"use client\";\n\nimport { WheelCore } from \"./GenreTreeWheelBase\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\n\nexport interface GenreTreeWheelProps extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the selected root changes — on mount with the default selection, and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. Stays upright and\n * fixed regardless of the wheel's rotation. */\n centerLabel?: string;\n /** When false, clicking a chip still selects its root and swaps in its subtree, but the wheel\n * itself stays at its current rotation instead of spinning the selected chip to the anchor.\n * Defaults to true. */\n allowWheelRotation?: boolean;\n}\n\n/**\n * All root genres distributed evenly around a wheel hugging the bottom of its container.\n * Clicking a chip rotates the wheel so that root lands at the top-center, and swaps in its\n * subtree — rendered as a single vertical `<GenreTree>` growing upward from that anchor.\n * Only the selected root's subtree is ever mounted.\n */\nexport function GenreTreeWheel(props: GenreTreeWheelProps) {\n return <WheelCore {...props} direction=\"bottom\" />;\n}\n","\"use client\";\n\nimport { WheelCore } from \"./GenreTreeWheelBase\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\n\nexport interface GenreTreeWheelRightProps extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the selected root changes — on mount with the default selection, and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. Stays upright and\n * fixed regardless of the wheel's rotation. */\n centerLabel?: string;\n /** When false, clicking a chip still selects its root and swaps in its subtree, but the wheel\n * itself stays at its current rotation instead of spinning the selected chip to the anchor.\n * Defaults to true. */\n allowWheelRotation?: boolean;\n}\n\n/**\n * All root genres distributed evenly around a wheel hugging the left edge of its container.\n * Clicking a chip rotates the wheel so that root lands at the right-center, and swaps in its\n * subtree — rendered as a single `<GenreTree>` growing rightward from that anchor.\n * Only the selected root's subtree is ever mounted.\n */\nexport function GenreTreeWheelRight(props: GenreTreeWheelRightProps) {\n return <WheelCore {...props} direction=\"left\" />;\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as d3 from \"d3\";\n\nimport { GenreTreeRootGroup, groupNodesByRoot } from \"./root-grouping\";\nimport { useTreeIndex } from \"./use-tree-index\";\nimport {\n buildCoreHierarchy,\n calculateCoreSubtreeRadialExtent,\n computeCoreRadialLayout,\n} from \"./core-radial-layout\";\nimport {\n getRadialPointOnCircle,\n POP_WEDGE_SPAN_DEGREES,\n renderPopSubtree,\n} from \"./pop-core-radial-layout\";\nimport { splitRootGroupBySide } from \"./pop-core-split\";\nimport {\n buildSectorClipPathPolygon,\n calculateWheelRadiusForAngles,\n computeRadialLayout,\n computeSectorWidths,\n RadialSlot,\n subdivideWedge,\n} from \"./radial-wheel-geometry\";\nimport { FloatingControls } from \"./FloatingControls\";\nimport { usePanZoom } from \"./use-pan-zoom\";\nimport { useNodeInfoPanel } from \"./use-node-info-panel\";\nimport { resolveInfoPanelObscuredArea } from \"./info-panel-geometry\";\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\nimport {\n ACCENT_TEXT_COLOR,\n calculateNodeDimensions,\n calculateNodeFontSize,\n getGenreTreeColor,\n getItemCountRange,\n hexToRgba,\n INFO_PANEL_WIDTH,\n MAX_NODE_WIDTH,\n PER_TREE_ACCENT_DOT,\n POP_TREE_DEPTH_RADIAL_SPACING,\n ROOT_SECTOR_FILL_OPACITY,\n WHEEL_RADIUS,\n WHEEL_ROTATION_EASING,\n WHEEL_ROTATION_TRANSITION_MS,\n ZOOM_FOCUS_SCALE,\n} from \"./constants\";\n\nexport interface WheelRadialCoreProps\n extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the top (just-clicked) root changes — on mount with the default selection,\n * and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. */\n centerLabel?: string;\n}\n\n// The wheel always lands the just-clicked root on the right (matches GenreTreeWheelRight's own\n// landingAngle=90 convention) — see computeRadialLayout's doc comment for why.\nconst LANDING_ANGLE = 90;\n\n// Unwraps computeRadialLayout's [0, 360)-wrapped angles into each root's own continuous\n// (possibly >360 or negative) angle, nearest to its `previous` value — see the doc comment where\n// this is used in WheelRadialCore for why plain wrapped angles cause a wrong-direction transition.\nfunction computeContinuousAngles(\n groups: GenreTreeRootGroup[],\n layout: RadialSlot[],\n previous: Map<string, number>,\n): Map<string, number> {\n const angles = new Map<string, number>();\n groups.forEach((group, index) => {\n const rawAngle = layout[index]?.angle ?? 0;\n const previousAngle = previous.get(group.root.id);\n const laps =\n previousAngle === undefined\n ? 0\n : Math.round((previousAngle - rawAngle) / 360);\n angles.set(group.root.id, rawAngle + 360 * laps);\n });\n return angles;\n}\n\n/**\n * All root genres distributed around a full circle centered in its container, each one's angular\n * width proportional to its own subtree's node count (see `computeRadialLayout`), every one of\n * them fully developed (full subtree mounted) at once. Clicking any chip re-lays-out the whole\n * ring so that root lands on the right and recalculates every other root's angle from scratch.\n * Unlike `WheelCore`, there's no single wheel-wide rotation to animate — every root's angle is\n * recomputed fresh from `(rootWeights, topIndex, LANDING_ANGLE)` each render, then unwrapped\n * per chip (see `continuousAngleByRootId`) so each chip's own CSS transition always takes its own\n * shortest path instead of snapping through a 360deg/0deg wraparound.\n *\n * Each root's own subtree fans outward from the wheel's circle in polar coordinates (see\n * `computeCoreRadialLayout`), within an 80deg wedge centered on the root's angle — the same wedge\n * geometry and shared `<svg>` layer used by the pop/core wheel.\n */\nexport function WheelRadialCore({\n nodes,\n className,\n onRootSelect,\n centerLabel,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n showToolbar = true,\n renderInfoPanelBody,\n onSelectedNodeChange,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n itemCountScaleMax,\n nodeCountForSizing,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n wheelZoom,\n defaultViewport,\n onViewportChange,\n controlsOrientation = \"vertical\",\n showZoomControls = \"auto\",\n floatingControlsLeading,\n busy,\n selectedNodeId,\n}: WheelRadialCoreProps) {\n const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;\n const index = useTreeIndex(nodes);\n const groups = useMemo(() => groupNodesByRoot(nodes), [nodes]);\n const rootColorOf = (node: GenreTreeNode) =>\n getGenreTreeColor(index.byId.get(index.rootIdById.get(node.id) ?? node.id)?.wikidataId);\n\n const [topRootId, setTopRootId] = useState<string | null>(\n groups[0]?.root.id ?? null,\n );\n const viewportRef = useRef<HTMLDivElement>(null);\n const stageRef = useRef<HTMLDivElement>(null);\n const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });\n const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });\n // Read via a ref inside the D3 render effect below rather than depending on `panZoom` directly —\n // its identity changes every render, which would otherwise re-run (and re-mount the whole D3\n // tree) on every pan/zoom, exactly what that effect's own dependency list is designed to avoid.\n const centerOnElementRef = useRef(panZoom.centerOnElement);\n useEffect(() => {\n centerOnElementRef.current = panZoom.centerOnElement;\n });\n const showNodeInfoRef = useRef(showNodeInfo);\n useEffect(() => {\n showNodeInfoRef.current = showNodeInfo;\n });\n const wheelCircleRef = useRef<HTMLDivElement>(null);\n const coreSvgRef = useRef<SVGSVGElement>(null);\n\n // Falls back to the first root without writing state back when the explicitly selected root\n // disappears from `nodes` — avoids a setState-in-effect cascading render for derived state.\n const effectiveTopRootId = groups.some((group) => group.root.id === topRootId)\n ? topRootId\n : (groups[0]?.root.id ?? null);\n const topIndex = Math.max(\n groups.findIndex((group) => group.root.id === effectiveTopRootId),\n 0,\n );\n\n // Weighted by each root's own rendered core branch (splitRootGroupBySide), not group.nodes.length\n // — group.nodes also includes the root's pop branch (see coreHierarchyByRootId below), which this\n // component never renders, so weighting by it would inflate a root's slice past what it actually\n // draws whenever it has a large hidden pop branch.\n const rootWeights = useMemo(\n () => groups.map((group) => splitRootGroupBySide(group).coreNodes.length),\n [groups],\n );\n\n const layout = useMemo(\n () => computeRadialLayout(rootWeights, topIndex, LANDING_ANGLE),\n [rootWeights, topIndex],\n );\n\n // computeRadialLayout always returns angles wrapped to [0, 360) — re-rendering with a fresh\n // wrapped value on every selection change means a chip landing back near 0deg (top) has to\n // transition from e.g. 270deg down through 90/0, i.e. always counterclockwise, regardless of\n // which way the ring conceptually just turned. Track each chip's own last displayed (unwrapped)\n // angle and add/subtract full turns so its CSS transition always takes its own shortest path.\n // Recomputed by comparing against `layout` right in the render body (React's sanctioned\n // \"adjusting state during render\" pattern — see the React docs on storing info from previous\n // renders) rather than in an effect, so there's no extra committed frame with stale angles.\n const [angleMemo, setAngleMemo] = useState(() => ({\n layout,\n angles: computeContinuousAngles(groups, layout, new Map()),\n }));\n if (angleMemo.layout !== layout) {\n setAngleMemo({\n layout,\n angles: computeContinuousAngles(groups, layout, angleMemo.angles),\n });\n }\n const continuousAngleByRootId = angleMemo.angles;\n\n const rootItemCountById = useMemo(\n () => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),\n [groups],\n );\n\n const rootItemCountRange = useMemo(\n () =>\n getItemCountRange(\n groups.map((group) => ({\n itemCount: rootItemCountById.get(group.root.id)!,\n })),\n { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing },\n ),\n [groups, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing],\n );\n\n // Node cards scale against the whole tree's item counts, not just their own subtree's — a\n // shallow root's few nodes can have a narrow itemCount spread that would otherwise get stretched\n // across the full size range and render wildly inconsistent card sizes next to deeper siblings.\n const wheelItemCountRange = useMemo(\n () => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),\n [nodes, itemCountScaleMax, fullNodeCountForSizing],\n );\n\n // Real angular sector each ring root owns, proportional to its own weight (rootWeights) out of\n // the total — same widths computeRadialLayout placed chips with, so a root's wedge below is\n // guaranteed to fit within its actual sector without spilling into a neighbor's, regardless of\n // how uneven neighboring roots' weights are (see computeSectorWidths's doc comment).\n const sectorSpanByRootId = useMemo(() => {\n const map = new Map<string, number>();\n if (groups.length <= 1) return map;\n const widths = computeSectorWidths(rootWeights);\n groups.forEach((group, index) => {\n map.set(group.root.id, widths[index]);\n });\n return map;\n }, [groups, rootWeights]);\n\n const wedgeSpanForRoot = useCallback(\n (rootId: string) =>\n Math.min(\n POP_WEDGE_SPAN_DEGREES,\n sectorSpanByRootId.get(rootId) ?? POP_WEDGE_SPAN_DEGREES,\n ),\n [sectorSpanByRootId],\n );\n\n // Each root's own single angle on the ring — a link from the root down to any of its branches\n // must originate from the root's own unique chip position, not a branch's sub-wedge angle.\n const rootAngleByRootId = useMemo(() => {\n const map = new Map<string, number>();\n groups.forEach((group, index) => {\n map.set(group.root.id, layout[index]?.angle ?? 0);\n });\n return map;\n }, [groups, layout]);\n\n // Only roots that actually have children get a hierarchy built — a childless root has nothing\n // to fan outward. A root's pop-side branch (splitRootGroupBySide) is excluded here the same way\n // GenreTreeWheelRadialPopCoreBase excludes it, since this component has no separate pop\n // rendering path and mixing both branches' direct children into buildCoreHierarchy would leave\n // more than one node with no in-set parent — d3.stratify() rejects that as \"multiple roots\". A\n // root's wedge (capped at POP_WEDGE_SPAN_DEGREES) is subdivided across its own core branches,\n // weighted by each branch's node count — mirrors how ring roots themselves share the full circle.\n const coreHierarchyByRootId = useMemo(() => {\n const map = new Map<\n string,\n { hierarchy: d3.HierarchyNode<GenreTreeNode>; angle: number; span: number }[]\n >();\n groups.forEach((group) => {\n const { coreBranches } = splitRootGroupBySide(group);\n if (coreBranches.length === 0) return;\n const rootAngle = rootAngleByRootId.get(group.root.id)!;\n const wedgeSpan = wedgeSpanForRoot(group.root.id);\n const slices = subdivideWedge(\n rootAngle,\n wedgeSpan,\n coreBranches.map((branch) => branch.nodes.length),\n );\n map.set(\n group.root.id,\n coreBranches.map((branch, branchIndex) => ({\n hierarchy: buildCoreHierarchy(d3, branch.nodes),\n angle: slices[branchIndex]!.centerDeg,\n span: slices[branchIndex]!.spanDeg,\n })),\n );\n });\n return map;\n }, [groups, rootAngleByRootId, wedgeSpanForRoot]);\n\n // Floor every ring root sits on, driven purely by chip clearance — the base every root's\n // core wedge measures outward from, before the deepest developed subtree's extent is folded in.\n const chipClearanceFloor = useMemo(\n () =>\n calculateWheelRadiusForAngles(\n layout.map((slot) => slot.angle),\n wheelItemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH,\n WHEEL_RADIUS,\n ),\n [layout, wheelItemCountRange.maxNodeWidth],\n );\n\n // How far past the ring roots' own circle the deepest developed root's subtree reaches —\n // computed as a delta (base radius 0) since the actual base (wheelRadius) isn't known yet;\n // folded into wheelRadius below, then re-applied at the real base for rendering.\n const maxCoreExtentDelta = useMemo(() => {\n let extent = 0;\n coreHierarchyByRootId.forEach((branches) => {\n branches.forEach(({ hierarchy }) => {\n extent = Math.max(\n extent,\n calculateCoreSubtreeRadialExtent(\n hierarchy,\n POP_TREE_DEPTH_RADIAL_SPACING,\n 0,\n ),\n );\n });\n });\n return extent;\n }, [coreHierarchyByRootId]);\n\n const wheelRadius = useMemo(\n () => Math.max(chipClearanceFloor, chipClearanceFloor + maxCoreExtentDelta),\n [chipClearanceFloor, maxCoreExtentDelta],\n );\n\n // Read via a ref rather than depending on `onRootSelect` directly — consumers commonly pass an\n // inline callback, which would otherwise re-fire this effect (and any state it sets) every render.\n const onRootSelectRef = useRef(onRootSelect);\n useEffect(() => {\n onRootSelectRef.current = onRootSelect;\n });\n\n useEffect(() => {\n if (effectiveTopRootId) onRootSelectRef.current?.(effectiveTopRootId);\n }, [effectiveTopRootId]);\n\n useEffect(() => {\n if (!coreSvgRef.current) return;\n\n const svg = d3.select(coreSvgRef.current);\n svg.selectAll(\"*\").remove();\n const originGroup = svg\n .append(\"g\")\n .attr(\"transform\", `translate(${wheelRadius}, ${wheelRadius})`);\n\n coreHierarchyByRootId.forEach((branches, rootId) => {\n const rootLinkOrigin = getRadialPointOnCircle(\n rootAngleByRootId.get(rootId) ?? 0,\n wheelRadius,\n );\n\n const sectorGroup = originGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-core-sector\")\n .attr(\"data-gtv-root-id\", rootId);\n\n branches.forEach(({ hierarchy, angle, span }, branchIndex) => {\n const laidOut = computeCoreRadialLayout(\n d3,\n hierarchy,\n angle,\n span,\n wheelRadius,\n POP_TREE_DEPTH_RADIAL_SPACING,\n );\n const reparentForbiddenIds = reparentingNodeId\n ? (laidOut\n .descendants()\n .find((d) => d.data.id === reparentingNodeId)\n ?.descendants()\n .map((d) => d.data.id) ?? [])\n : [];\n\n const branchGroup = sectorGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-core-branch\")\n .attr(\"data-gtv-branch-index\", branchIndex);\n\n renderPopSubtree(\n d3,\n branchGroup,\n laidOut,\n getGenreTreeColor(index.byId.get(rootId)?.wikidataId),\n reparentingNodeId,\n reparentForbiddenIds,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId)\n void onReparent?.(reparentingNodeId, newParentId);\n },\n onNodeClick: (data, event) => {\n const clickedElement = event.currentTarget as Element | null;\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n additionalActions,\n playingNodeId,\n playState,\n },\n wheelItemCountRange,\n {\n isCoreSector: true,\n rootLinkOrigin,\n selectedNodeId: panel?.node.id ?? null,\n },\n );\n });\n });\n }, [\n index.byId,\n coreHierarchyByRootId,\n wheelRadius,\n rootAngleByRootId,\n reparentingNodeId,\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n wheelItemCountRange,\n panel?.node.id,\n ]);\n\n // Starts the view fit to the wheel + rendered subtrees instead of at scale 1 / pan (0, 0) —\n // guarded so it only fires once anchored content has actually rendered, and never again\n // afterward so it doesn't fight the user's own pan/zoom on later selections.\n const hasInitialFitRef = useRef(false);\n useEffect(() => {\n if (hasInitialFitRef.current) return;\n const elements = [wheelCircleRef.current, coreSvgRef.current];\n if (!elements.some(Boolean)) return;\n hasInitialFitRef.current = true;\n panZoom.fitToFrame(elements, { silent: true });\n });\n\n const handleChipClick = (rootId: string) => {\n setTopRootId(rootId);\n };\n\n // Every root's core subtree is always mounted (see the SVG-building effect above), so an\n // externally-controlled selection (e.g. from a search result) never needs a root swap — just\n // locate its element and open the panel exactly as a direct click would.\n useEffect(() => {\n if (!selectedNodeId || panel?.node.id === selectedNodeId) return;\n const targetNode = index.byId.get(selectedNodeId);\n const element = viewportRef.current?.querySelector(\n `#group-${CSS.escape(selectedNodeId)}`,\n );\n if (!targetNode || !element) return;\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(targetNode, element, viewportRef.current, { notify: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps -- panZoom re-created on pan/zoom; guarded by the panel?.node.id check above\n }, [selectedNodeId, index, panel, showNodeInfo]);\n\n // One divider per boundary between two angularly-adjacent roots — each root's own continuous\n // (unwrapped) angle plus half its weight-proportional width (sectorSpanByRootId) lands exactly on\n // the boundary with its next neighbor, since computeRadialLayout tiled the roots' widths\n // contiguously around the circle in the first place.\n const dividerAngles = useMemo(() => {\n if (groups.length <= 1) return [];\n return groups.map((group) => {\n const angle = continuousAngleByRootId.get(group.root.id) ?? 0;\n const width = sectorSpanByRootId.get(group.root.id) ?? 0;\n return angle + width / 2;\n });\n }, [groups, continuousAngleByRootId, sectorSpanByRootId]);\n\n // One tinted sector fan per root, bounded by its own weight-proportional width rather than\n // bisected neighbor angles — see computeSectorWidths's doc comment for why bisection\n // under/over-shoots a root's true sector when its neighbors' weights differ a lot from its own.\n const sectorFills = useMemo(() => {\n if (groups.length <= 1) return [];\n return groups.map((group) => {\n const angle = continuousAngleByRootId.get(group.root.id) ?? 0;\n const width = sectorSpanByRootId.get(group.root.id) ?? 0;\n return {\n rootId: group.root.id,\n start: angle - width / 2,\n clipPath: buildSectorClipPathPolygon(width),\n color: hexToRgba(\n getGenreTreeColor(group.root.wikidataId),\n ROOT_SECTOR_FILL_OPACITY,\n ),\n };\n });\n }, [groups, continuousAngleByRootId, sectorSpanByRootId]);\n\n return (\n <div\n ref={viewportRef}\n aria-busy={busy}\n className={[\n \"gtv-wheel-container\",\n \"gtv-wheel-container--radial\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n \"--gtv-wheel-radius\": `${wheelRadius}px`,\n \"--gtv-wheel-rotation-transition-ms\": `${WHEEL_ROTATION_TRANSITION_MS}ms`,\n \"--gtv-wheel-rotation-easing\": WHEEL_ROTATION_EASING,\n touchAction: panZoom.touchAction,\n } as React.CSSProperties\n }\n onPointerDown={panZoom.handlePointerDown}\n >\n {/* height: 100% (not just width) so this is the containing block the radial stage's own\n top/left: 50% measure against — see the equivalent comment on GenreTreeWheelBase. */}\n <div\n ref={stageRef}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n transform: panZoom.transform,\n transformOrigin: \"0 0\",\n }}\n >\n <div className=\"gtv-wheel-stage\">\n <div className=\"gtv-wheel-circle\" ref={wheelCircleRef} />\n\n {/* Sector wash sits in its own .gtv-wheel layer, rendered before the node svg below, so\n the wash paints under the developed nodes instead of dulling them — see the second\n .gtv-wheel layer (after the svg) for the dividers/chips, which stay above the nodes. */}\n <div className=\"gtv-wheel\">\n {sectorFills.map((sector) => (\n <div\n key={`sector-${sector.rootId}`}\n className=\"gtv-wheel-sector\"\n style={\n {\n \"--gtv-sector-angle\": `${sector.start}deg`,\n \"--gtv-sector-color\": sector.color,\n clipPath: sector.clipPath,\n } as React.CSSProperties\n }\n />\n ))}\n </div>\n\n <svg\n ref={coreSvgRef}\n className=\"gtv-wheel-pop-layer\"\n width={wheelRadius * 2}\n height={wheelRadius * 2}\n />\n\n {centerLabel && (\n <div className=\"gtv-wheel-center-label\">{centerLabel}</div>\n )}\n\n <div className=\"gtv-wheel\">\n {dividerAngles.map((angle, index) => (\n <div\n key={`divider-${groups[index].root.id}`}\n className=\"gtv-wheel-divider\"\n style={\n {\n \"--gtv-divider-angle\": `${angle}deg`,\n } as React.CSSProperties\n }\n />\n ))}\n\n {groups.map((group, index) => {\n const slot = layout[index];\n const angle =\n continuousAngleByRootId.get(group.root.id) ?? slot?.angle ?? 0;\n const chipColor = getGenreTreeColor(group.root.wikidataId);\n const rootItemCount = rootItemCountById.get(group.root.id)!;\n const dimensions = calculateNodeDimensions(\n rootItemCount,\n rootItemCountRange,\n );\n const fontSize = calculateNodeFontSize(\n rootItemCount,\n rootItemCountRange,\n );\n return (\n <div\n key={group.root.id}\n className=\"gtv-wheel-slot\"\n style={\n { \"--gtv-chip-angle\": `${angle}deg` } as React.CSSProperties\n }\n >\n <div className=\"gtv-wheel-chip-anchor\">\n <button\n type=\"button\"\n id={`group-${group.root.id}`}\n className=\"gtv-wheel-chip gtv-wheel-chip--selected\"\n style={\n {\n width: dimensions.WIDTH,\n // Height comes from --gtv-wheel-chip-base-height (see .gtv-wheel-chip in\n // styles.css), not an inline `height`/`minHeight` — an inline `height` always\n // wins the cascade over the CSS class's `height: calc(...)` hover override\n // (inline style beats any stylesheet rule), silently pinning the box and\n // breaking the hover growth again, same failure mode as before.\n \"--gtv-wheel-chip-base-height\": `${dimensions.HEIGHT}px`,\n \"--gtv-chip-color\": chipColor,\n } as React.CSSProperties\n }\n onClick={(event) => {\n panZoom.centerOnElement(\n event.currentTarget,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(event.currentTarget, viewportRef.current, INFO_PANEL_WIDTH),\n );\n handleChipClick(group.root.id);\n if (!reparentingNodeId) {\n showNodeInfo(\n group.root,\n event.currentTarget,\n viewportRef.current,\n );\n onNodeClick?.(group.root, event.nativeEvent);\n }\n }}\n >\n {PER_TREE_ACCENT_DOT && (\n <span className=\"gtv-wheel-chip-dot\" />\n )}\n <span\n className=\"gtv-node-label gtv-node-label--root\"\n style={{ fontSize }}\n >\n {group.root.name}\n </span>\n </button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {panel && (\n <InfoPanel\n node={panel.node}\n fill={rootColorOf(panel.node)}\n textColor={ACCENT_TEXT_COLOR}\n parentNode={(() => {\n const parent = index.byId.get(panel.node.parentId ?? \"\");\n return parent\n ? {\n node: parent,\n fill: rootColorOf(parent),\n textColor: ACCENT_TEXT_COLOR,\n }\n : null;\n })()}\n // renderPopSubtree is always called here with isCoreSector: true (no pop/core split in\n // this renderer), so every node gets a solid rootColor fill.\n childNodes={(index.childrenByParentId.get(panel.node.id) ?? []).map(\n (n) => ({ node: n, fill: rootColorOf(n), textColor: ACCENT_TEXT_COLOR }),\n )}\n ancestorNodes={index.ancestorsOf(panel.node.id).map(\n (n): InfoPanelChild => ({\n node: n,\n fill: rootColorOf(n),\n textColor: ACCENT_TEXT_COLOR,\n }),\n )}\n side={panel.side}\n onClose={closeNodeInfo}\n onSelectNode={(id) => {\n const targetNode = index.byId.get(id)!;\n const element = viewportRef.current!.querySelector(\n `#group-${CSS.escape(id)}`,\n );\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(\n targetNode,\n element ?? viewportRef.current,\n viewportRef.current,\n );\n }}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n renderInfoPanelBody={renderInfoPanelBody}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n }\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n\n <FloatingControls\n panZoom={panZoom}\n orientation={controlsOrientation}\n zoomControls={showZoomControls}\n leading={floatingControlsLeading}\n busy={busy}\n onFit={() => panZoom.fitToFrame([wheelCircleRef.current, coreSvgRef.current])}\n />\n </div>\n );\n}\n","import { DOUBLE_CLICK_DELAY_MS } from \"./constants\";\n\n/** Runs a click's action only once no second click follows, so the pan/zoom glide it starts can't\n * move the target out from under the cursor before `dblclick` fires. */\nexport function createDeferredClick() {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let pendingKey: unknown = undefined;\n let doubledAt = -Infinity;\n const cancel = () => {\n if (timeoutId !== null) clearTimeout(timeoutId);\n timeoutId = null;\n };\n /** With `onDouble`, a second click on the same `key` while the first is pending runs it instead:\n * touch browsers don't reliably fire `dblclick` for a double-tap. */\n const schedule = (action: () => void, onDouble?: () => void, key?: unknown) => {\n if (onDouble && timeoutId !== null && key === pendingKey) {\n cancel();\n doubledAt = Date.now();\n onDouble();\n return;\n }\n cancel();\n pendingKey = key;\n timeoutId = setTimeout(() => {\n timeoutId = null;\n action();\n }, DOUBLE_CLICK_DELAY_MS);\n };\n /** The native `dblclick` handler: skipped when `schedule` already ran `onDouble` for this pair. */\n const doubleClick = (onDouble: () => void) => {\n cancel();\n if (Date.now() - doubledAt < DOUBLE_CLICK_DELAY_MS) return;\n onDouble();\n };\n return { schedule, cancel, doubleClick };\n}\n","export interface RadialSlot {\n rootIndex: number;\n angle: number;\n}\n\n/** Full radial layout for `weights.length` roots (fixed ring order) with `clickedIndex` landing at\n * `landingAngle` (CSS `rotate()` convention: 0 = top, clockwise; 90 = right, this wheel's\n * confirmed landing direction). Every root gets an arc proportional to its own `weights` entry\n * (its subtree's node count) rather than an equal `360/rootCount` share — a root with more nodes\n * occupies proportionally more of the circle. Ring order is unchanged: roots are still laid out\n * in order starting at the clicked root, walking the ring from `clickedIndex`. Each root's angle\n * is the center of its own proportionally-sized arc, so the clicked root's arc is centered exactly\n * on `landingAngle`, matching the even-spacing case's guarantee that the clicked root's own angle\n * equals `landingAngle`. Every root is fully developed. Returns one entry per ring index\n * (0..weights.length-1), indexable by the same index groupNodesByRoot's output uses. Empty for\n * `weights.length <= 0`. Every weight must be positive (a root's own weight is its subtree's node\n * count, which always includes the root itself, so a zero/negative weight indicates invalid\n * input rather than a real, reachable state) — throws otherwise rather than silently coercing it. */\nexport function computeRadialLayout(weights: number[], clickedIndex: number, landingAngle: number): RadialSlot[] {\n const rootCount = weights.length;\n if (rootCount <= 0) return [];\n\n if (weights.some((weight) => weight <= 0)) {\n throw new Error(\"computeRadialLayout: every root weight (node count) must be positive\");\n }\n\n const total = weights.reduce((sum, weight) => sum + weight, 0);\n const orderedWidths = weights.map((_, i) => (weights[(clickedIndex + i) % rootCount] / total) * 360);\n\n const slots: RadialSlot[] = new Array(rootCount);\n let boundary = landingAngle - orderedWidths[0] / 2;\n for (let i = 0; i < rootCount; i++) {\n const width = orderedWidths[i];\n const rootIndex = (clickedIndex + i) % rootCount;\n slots[rootIndex] = { rootIndex, angle: (((boundary + width / 2) % 360) + 360) % 360 };\n boundary += width;\n }\n\n return slots;\n}\n\n/** Each root's true angular sector width in degrees, proportional to its own `weights` entry out\n * of the total — the same per-root share `computeRadialLayout` uses internally to place chips, but\n * exposed directly so callers reconstructing a root's sector bounds (dividers, colored wash, wedge\n * caps) can derive them from the actual weights instead of bisecting neighboring roots' angles\n * (which only approximates the true bounds, and badly so when neighbors' weights differ a lot from\n * the root's own — see computeSectorBounds). */\nexport function computeSectorWidths(weights: number[]): number[] {\n const total = weights.reduce((sum, weight) => sum + weight, 0);\n return weights.map((weight) => (weight / total) * 360);\n}\n\n/** Splits a wedge (centered on `wedgeCenterDeg`, spanning `wedgeSpanDeg`) into `weights.length`\n * sub-wedges, each proportional to its own weight — same proportional-width logic as\n * `computeSectorWidths`, applied within a wedge instead of the full 360deg circle. A wedge, unlike\n * the full ring, never wraps past 360, so sub-wedges are simply laid out left to right from the\n * wedge's own start angle. Returns each sub-wedge's own center angle and span, in the same order\n * as `weights`. Empty for `weights.length <= 0`. */\nexport function subdivideWedge(\n wedgeCenterDeg: number,\n wedgeSpanDeg: number,\n weights: number[],\n): { centerDeg: number; spanDeg: number }[] {\n if (weights.length <= 0) return [];\n\n const total = weights.reduce((sum, weight) => sum + weight, 0);\n const spans = weights.map((weight) => (weight / total) * wedgeSpanDeg);\n\n const slices: { centerDeg: number; spanDeg: number }[] = [];\n let boundary = wedgeCenterDeg - wedgeSpanDeg / 2;\n for (const spanDeg of spans) {\n slices.push({ centerDeg: boundary + spanDeg / 2, spanDeg });\n boundary += spanDeg;\n }\n return slices;\n}\n\n/** Midpoint angle between two chip angles that may be continuous/unwrapped (e.g. from\n * computeContinuousAngles), taking the shorter arc between them rather than always going the\n * increasing-angle way — brings `b` within 180 degrees of `a` before averaging, so the divider\n * sits between the two chips visually instead of on the far side of the circle. Used to place a\n * radial divider on the boundary between two angularly-adjacent roots. */\nexport function bisectAngles(a: number, b: number): number {\n const diff = b - a;\n const adjustedB = diff > 180 ? b - 360 : diff < -180 ? b + 360 : b;\n return (a + adjustedB) / 2;\n}\n\n/** `clip-path: polygon(...)` value for a filled circular-sector fan spanning `[0, sweepDeg]`,\n * measured clockwise from local \"up\" (CSS `rotate()` convention) — meant for a huge square element\n * centered on the wheel's pivot (same oversized-element + `.gtv-wheel-container` overflow: hidden\n * clipping trick as `.gtv-wheel-divider`), then rotated to its sector's own start angle via\n * `transform: rotate()`, same as a divider line. Interpolates a point every `stepDeg` (default 10)\n * around the arc rather than just the two end rays: a naive 3-point triangle (center + two far\n * points) chord-clips away the middle of any sector wider than ~180 degrees, which every root gets\n * when there are only 2. Radius 50% (of the element's own box) reaches that huge element's edge,\n * same as the box being exactly 2x oversized in every direction from center. Empty string (no\n * shape / nothing rendered) for a non-positive sweep. */\nexport function buildSectorClipPathPolygon(sweepDeg: number, stepDeg = 10): string {\n if (sweepDeg <= 0) return \"\";\n const points: string[] = [\"50% 50%\"];\n const steps = Math.max(1, Math.ceil(sweepDeg / stepDeg));\n for (let i = 0; i <= steps; i++) {\n const theta = (Math.min(i * stepDeg, sweepDeg) * Math.PI) / 180;\n const x = 50 + 50 * Math.sin(theta);\n const y = 50 - 50 * Math.cos(theta);\n points.push(`${x}% ${y}%`);\n }\n return `polygon(${points.join(\", \")})`;\n}\n\n/** Start/end angle (continuous, CSS `rotate()` convention — may exceed 360 or go negative, same as\n * `continuousAngleByRootId`) of the sector belonging to the root at `index` in `continuousAngles`\n * (ring order). Both bounds are computed anchored on that root's own continuous angle (`bisectAngles(mine, neighbor)`,\n * not the reverse) so they land in the same unwrapped frame as each other regardless of how far\n * `continuousAngleByRootId` has wound up over prior re-layouts — `end - start` is always the\n * sector's true sweep instead of an arbitrary multiple of 360 off, unlike naively differencing two\n * independently-anchored `dividerAngles` entries.\n *\n * n === 2 is special-cased: with only one other root, `prev` and `next` are the same root, so the\n * general formula above degenerates to a zero-width sector (both bounds land on the same\n * shortest-arc bisection). There, the single boundary between the two roots and its opposite point\n * (+/- 180) are each root's two bounds — same as the two (coincident) entries dividerAngles\n * produces for n === 2, just split into the two 180-degree halves they actually imply. */\nexport function computeSectorBounds(\n continuousAngles: number[],\n index: number,\n): { start: number; end: number } {\n const n = continuousAngles.length;\n const mine = continuousAngles[index];\n\n if (n === 2) {\n const other = continuousAngles[(index + 1) % 2];\n const boundary = bisectAngles(mine, other);\n return mine < boundary ? { start: boundary - 180, end: boundary } : { start: boundary, end: boundary + 180 };\n }\n\n const prev = continuousAngles[(index - 1 + n) % n];\n const next = continuousAngles[(index + 1) % n];\n return { start: bisectAngles(mine, prev), end: bisectAngles(mine, next) };\n}\n\n/** Widest symmetric wedge, centered on the root's own angle (`continuousAngles[index]`), that still\n * fits entirely within that root's actual angular sector (see `computeSectorBounds`) — twice the\n * smaller of the two distances from the root's own angle to each of its sector's bounds. A sector\n * isn't generally symmetric around the root's own angle (neighbors can sit at uneven angular\n * distances, e.g. when `computeRadialLayout` gives roots weight-proportional arc widths), so capping\n * a symmetric wedge at the raw sector width (`end - start`) lets the wider side spill past its\n * boundary into the neighboring sector even though the narrower side has no slack to give. */\nexport function computeSectorSymmetricSpan(continuousAngles: number[], index: number): number {\n const mine = continuousAngles[index];\n const { start, end } = computeSectorBounds(continuousAngles, index);\n return 2 * Math.min(mine - start, end - mine);\n}\n\n/** Generalizes calculateWheelRadius's chord-overlap guarantee (see wheel-geometry.ts) to\n * non-uniformly-spaced angles: the ring only needs to grow enough to clear the SMALLEST adjacent\n * gap actually present (including the wraparound gap from the last angle back to the first), not\n * a uniform 360/n gap. Falls back to baseRadius when there are fewer than 2 angles, since a\n * single chip has no neighbor to overlap. */\nexport function calculateWheelRadiusForAngles(angles: number[], maxChipWidth: number, baseRadius: number): number {\n if (angles.length < 2) return baseRadius;\n\n const sorted = [...angles].sort((a, b) => a - b);\n let minGapDeg = 360;\n for (let i = 0; i < sorted.length; i++) {\n const next = i < sorted.length - 1 ? sorted[i + 1] : sorted[0] + 360;\n minGapDeg = Math.min(minGapDeg, next - sorted[i]);\n }\n if (minGapDeg <= 0) return baseRadius;\n\n const CHIP_GAP = 16;\n const requiredRadius = (maxChipWidth + CHIP_GAP) / (2 * Math.sin((minGapDeg * Math.PI) / 360));\n return Math.max(baseRadius, requiredRadius);\n}\n\n/** Smallest radius ≥ `floorRadius` at which `angleNeededAt(radius)` (rad, non-increasing in\n * radius) fits the full circle — binary-searched since what a ring needs depends on its radius. */\nexport function solveRingRadius(angleNeededAt: (radius: number) => number, floorRadius: number): number {\n const fits = (radius: number) => angleNeededAt(radius) <= 2 * Math.PI;\n if (fits(floorRadius)) return floorRadius;\n let lo = floorRadius;\n let hi = floorRadius * 2;\n while (!fits(hi)) {\n lo = hi;\n hi *= 2;\n }\n while (hi - lo > 1) {\n const mid = (lo + hi) / 2;\n if (fits(mid)) hi = mid;\n else lo = mid;\n }\n return hi;\n}\n\nexport interface Box {\n x0: number;\n y0: number;\n x1: number;\n y1: number;\n}\n\nexport interface LabelCullItem {\n id: string;\n /** Candidate label boxes, in preference order. */\n labels: Box[];\n dot: Box;\n priority: number;\n /** Drawn even when no candidate is free: falls back to the first one and still blocks later labels. */\n forced?: boolean;\n}\n\nconst boxesOverlap = (a: Box, b: Box) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;\n\nconst GRID_CELL = 64;\n\n/** Uniform-grid index over boxes: `some` only visits boxes sharing a cell with the query box. */\nfunction createBoxGrid<T>() {\n const cells = new Map<string, { box: Box; value: T }[]>();\n const cellRange = (box: Box) => ({\n cx0: Math.floor(box.x0 / GRID_CELL),\n cx1: Math.floor(box.x1 / GRID_CELL),\n cy0: Math.floor(box.y0 / GRID_CELL),\n cy1: Math.floor(box.y1 / GRID_CELL),\n });\n return {\n insert(box: Box, value: T) {\n const { cx0, cx1, cy0, cy1 } = cellRange(box);\n for (let cx = cx0; cx <= cx1; cx++) {\n for (let cy = cy0; cy <= cy1; cy++) {\n const key = `${cx},${cy}`;\n const bucket = cells.get(key);\n if (bucket) bucket.push({ box, value });\n else cells.set(key, [{ box, value }]);\n }\n }\n },\n some(box: Box, predicate: (value: T) => boolean) {\n const { cx0, cx1, cy0, cy1 } = cellRange(box);\n for (let cx = cx0; cx <= cx1; cx++) {\n for (let cy = cy0; cy <= cy1; cy++) {\n if (cells.get(`${cx},${cy}`)?.some((entry) => boxesOverlap(box, entry.box) && predicate(entry.value))) {\n return true;\n }\n }\n }\n return false;\n },\n };\n}\n\n/** Index of each item's chosen label candidate, or -1 when it must be hidden: greedily, by\n * descending priority, takes each item's first candidate that overlaps no already-placed label and\n * no other node's dot (a `forced` item falls back to its first candidate instead of hiding). */\nexport function placeLabels(items: LabelCullItem[]): Map<string, number> {\n const placement = new Map<string, number>();\n const keptGrid = createBoxGrid<true>();\n const dotGrid = createBoxGrid<string>();\n for (const item of items) dotGrid.insert(item.dot, item.id);\n for (const item of [...items].sort((a, b) => b.priority - a.priority)) {\n let index = item.labels.findIndex(\n (label) => !keptGrid.some(label, () => true) && !dotGrid.some(label, (id) => id !== item.id),\n );\n if (index < 0 && item.forced && item.labels.length > 0) index = 0;\n placement.set(item.id, index);\n if (index >= 0) keptGrid.insert(item.labels[index], true);\n }\n return placement;\n}\n\n/** Id of the sector (degrees, `center ± span / 2`, any winding) containing `angle`, or null. */\nexport function findSectorAtAngle(\n angle: number,\n sectors: { id: string; center: number; span: number }[],\n): string | null {\n const sector = sectors.find(\n ({ center, span }) => Math.abs(((((angle - center) % 360) + 540) % 360) - 180) <= span / 2,\n );\n return sector?.id ?? null;\n}\n\nconst boxGap = (a: Box, b: Box) =>\n Math.hypot(Math.max(0, a.x0 - b.x1, b.x0 - a.x1), Math.max(0, a.y0 - b.y1, b.y0 - a.y1));\n\n/** Center of a `width` x `height` box inside `area` that overlaps no obstacle, sampled on a grid:\n * spots passing `prefer` first, then the widest gap to the nearest obstacle (capped at `height`, so\n * \"clear enough\" ties), then the one closest to the area's center. Null when every spot is blocked. */\n// ponytail: fixed 9x9 sampling grid, sample finer if labels visibly miss obvious gaps.\nexport function pickEmptyLabelSpot(\n area: Box,\n width: number,\n height: number,\n obstacles: Box[],\n prefer: (x: number, y: number) => boolean = () => true,\n): { x: number; y: number } | null {\n const STEPS = 9;\n const spanX = area.x1 - area.x0 - width;\n const spanY = area.y1 - area.y0 - height;\n if (spanX < 0 || spanY < 0) return null;\n const centerX = (area.x0 + area.x1) / 2;\n const centerY = (area.y0 + area.y1) / 2;\n let best: { x: number; y: number; score: [number, number, number] } | null = null;\n for (let i = 0; i < STEPS; i++) {\n for (let j = 0; j < STEPS; j++) {\n const x = area.x0 + width / 2 + (spanX * i) / (STEPS - 1);\n const y = area.y0 + height / 2 + (spanY * j) / (STEPS - 1);\n const box = { x0: x - width / 2, x1: x + width / 2, y0: y - height / 2, y1: y + height / 2 };\n if (obstacles.some((obstacle) => boxesOverlap(box, obstacle))) continue;\n const gap = Math.min(height, ...obstacles.map((obstacle) => boxGap(box, obstacle)));\n const score: [number, number, number] = [\n prefer(x, y) ? 1 : 0,\n gap,\n -Math.hypot(x - centerX, y - centerY),\n ];\n const k = best ? score.findIndex((value, index) => value !== best!.score[index]) : 0;\n if (!best || (k >= 0 && score[k] > best.score[k])) best = { x, y, score };\n }\n }\n return best && { x: best.x, y: best.y };\n}\n","import * as d3 from \"d3\";\n\nimport { createDeferredClick } from \"./defer-click\";\nimport { GenreTreeAction, GenreTreeNode, GenreTreePlayState } from \"./types\";\nimport {\n addReparentTargetOverlay,\n buildTreeHierarchyStructure,\n} from \"./NodeHelper\";\nimport {\n ACCENT_COLOR,\n ItemCountRange,\n MAX_NODE_WIDTH,\n NODE_DOT_LABEL_GAP,\n NODE_LAYOUT_SLOT_RADIUS,\n POP_SECTOR_TINT_RATIO,\n POP_TREE_DEPTH_RADIAL_SPACING,\n RADIAL_CORE_LINK_TINT_RATIO,\n RADIAL_POP_LINK_TINT_RATIO,\n SURFACE_BORDER_COLOR,\n TEXT_COLOR,\n calculateRootLabelFontSize,\n TEXT_MUTED_COLOR,\n calculateLinkWidth,\n calculateNodeBorderWidth,\n calculateNodeDotRadius,\n calculateSelectedNodeBorderWidth,\n calculateLabelFontSizeAtZoom,\n calculateLabelRank,\n calculateNodeFontSize,\n isLabelRankedForZoom,\n tintSurface,\n} from \"./constants\";\nimport {\n Box,\n placeLabels,\n} from \"./radial-wheel-geometry\";\n\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\ntype D3Selection = d3.Selection<SVGGElement, unknown, null, undefined>;\n\n// Angular width of the wedge a pop subtree fans out into, centered on its own root's direction —\n// narrower than the root's own full sector so it never touches a neighboring root's wedge,\n// matching the gutter every other radial-wheel element keeps from its neighbors.\nexport const POP_WEDGE_SPAN_DEGREES = 80;\n\n// Margin (px) added past a pop subtree's deepest node's own half-width, so its rendered card\n// never sits flush against the wheel's own circle edge.\nexport const POP_SUBTREE_OUTER_MARGIN = 24;\n\n// A pop hierarchy (per buildPopHierarchy/splitRootGroupBySide) is rooted at the pop child, which\n// is always the ring root's direct child — i.e. always absolute depth 1 in the whole tree,\n// regardless of which root it belongs to.\nconst POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH = 1;\nconst DOT_RADIUS_TRANSITION_MS = 250;\n\n/** The radius (px, from the wheel's true center) for a node at `depth` steps below the ring roots'\n * own circle (`coreRootCircleRadius`) — the single formula every radial depth (ring roots, their\n * pop branches, and the center \"Mainstream Pop\" subtree) shares, so that any two nodes at the same\n * absolute depth always land on the same circle regardless of which branch/subtree they belong to. */\nexport function getRadialDepthRadius(\n depth: number,\n coreRootCircleRadius: number,\n depthSpacing: number,\n): number {\n return coreRootCircleRadius + depth * depthSpacing;\n}\n\n/** Builds the pop subtree's hierarchy, rooted at the pop child (root not included) — thin wrapper\n * over buildTreeHierarchyStructure so callers of this module don't need to import NodeHelper\n * directly just for this one call. */\n// splitRootGroupBySide (pop-core-split.ts) returns the pop subtree rooted at the pop child, whose\n// own parentId still points at the (excluded) root — d3.stratify requires every parentId to\n// either be null or resolve within the given set, so that dangling reference is normalized to\n// null here, making the pop child stratify's root. Each resulting hierarchy node's `.data` is then\n// restored to its original (un-normalized) node so the real parentId still reaches\n// renderers/click handlers/the info panel.\nexport function buildPopHierarchy(\n d3Lib: typeof import(\"d3\"),\n popNodes: GenreTreeNode[],\n): D3Node {\n const ids = new Set(popNodes.map((node) => node.id));\n const byId = new Map(popNodes.map((node) => [node.id, node]));\n const normalized = popNodes.map((node) =>\n node.parentId !== null && !ids.has(node.parentId)\n ? { ...node, parentId: null }\n : node,\n );\n const hierarchy = buildTreeHierarchyStructure(d3Lib, normalized);\n hierarchy.each((d) => {\n d.data = byId.get(d.data.id) ?? d.data;\n });\n return hierarchy;\n}\n\n/** The outer radius (px, from `coreRootCircleRadius`) a pop subtree needs to render without\n * overlapping the wheel's own edge — driven by its deepest node's ring (absolute depth\n * `POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH + height`) plus that node's own half-width and a fixed outer\n * margin. Used to grow the wheel's circle to fit whichever developed root's pop subtree\n * reaches furthest.\n *\n * `coreRootCircleRadius` shifts the whole subtree outward by that amount, mirroring\n * `computePopRadialLayout`'s own base radius — pass 0 to get the extent as a delta past the ring\n * roots' own circle (e.g. when that circle's own size is still being determined from this delta),\n * or the wheel's actual `coreRootCircleRadius` to get the subtree's true outer radius. */\nexport function calculatePopSubtreeRadialExtent(\n hierarchy: D3Node,\n coreRootCircleRadius = 0,\n): number {\n return (\n getRadialDepthRadius(\n POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH + hierarchy.height,\n coreRootCircleRadius,\n POP_TREE_DEPTH_RADIAL_SPACING,\n ) +\n MAX_NODE_WIDTH / 2 +\n POP_SUBTREE_OUTER_MARGIN\n );\n}\n\n/**\n * Lays a pop subtree's hierarchy out in polar coordinates within a wedge centered on\n * `wedgeCenterAngleDegrees` (CSS `rotate()` convention: 0 = top, clockwise — same convention as\n * radial-wheel-geometry.ts's angles), then projects each node onto cartesian (x, y) relative to\n * the wheel's own center — writes the result directly onto each node's `.x`/`.y`, mirroring how\n * tree-renderer.ts's setupTreeLayout overwrites a d3 hierarchy's laid-out coordinates in place.\n *\n * Angle spread across siblings/cousins uses d3's tidy-tree balancing (`d3.tree()`), with each\n * neighbour gap measured from the two dots' real arc length on their ring (see `measureWedge`). Radius is NOT taken from that layout's own y — every node's radius climbs INWARD from\n * `coreRootCircleRadius` (the ring roots' own circle, where this branch's ring root chip sits) by\n * `POP_TREE_DEPTH_RADIAL_SPACING` per depth step, so the branch's own root (absolute depth\n * `POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH`) always lands exactly one depth step inside that circle\n * regardless of how deep the branch grows — anchoring outward from the mainstream circle instead\n * would leave shallower branches stranded partway to the ring roots' circle while only the single\n * deepest developed branch actually reached it. Any slack this leaves between the branch's own\n * deepest node and the mainstream circle is absorbed there, not by moving the branch's own root\n * off the ring roots' circle.\n *\n * `wedgeSpanDegrees` defaults to `POP_WEDGE_SPAN_DEGREES`; the wheel passes each branch's slice of\n * its root's sector, sized from `measureWedge`; the layout fills it up to `MAX_WEDGE_STRETCH` (see layoutWedge).\n *\n * `depthSpacing` defaults to `POP_TREE_DEPTH_RADIAL_SPACING` — kept constant per depth step\n * regardless of how far out the wheel's own outer circle ends up sitting; it's the outer circle's\n * own radius (`coreRootCircleRadius`, sized off the deepest developed pop branch's reach — see\n * `popReachRequiredRadius` in the wheel component) that adapts to fit the pop branches, not the\n * spacing between their depths. */\nexport function computePopRadialLayout(\n d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n wedgeCenterAngleDegrees: number,\n coreRootCircleRadius: number,\n wedgeSpanDegrees: number = POP_WEDGE_SPAN_DEGREES,\n depthSpacing: number = POP_TREE_DEPTH_RADIAL_SPACING,\n dotRadius: number = NODE_LAYOUT_SLOT_RADIUS,\n): D3Node {\n return layoutWedge(\n d3Lib,\n hierarchy,\n wedgeCenterAngleDegrees,\n wedgeSpanDegrees,\n popRadiusOfDepth(coreRootCircleRadius, depthSpacing),\n \"center\",\n dotRadius,\n );\n}\n\nexport const MIN_DOT_GAP = 5.6;\n\n/** Radius of a pop hierarchy's node at `depth`, climbing inward from the ring roots' circle. */\nexport function popRadiusOfDepth(coreRootCircleRadius: number, depthSpacing: number) {\n return (depth: number) =>\n getRadialDepthRadius(-(POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH + depth), coreRootCircleRadius, depthSpacing);\n}\n\n/** Radius of a core hierarchy's node at `depth`, fanning outward from the ring roots' circle. */\nexport function coreRadiusOfDepth(coreRootCircleRadius: number, depthSpacing: number) {\n return (depth: number) => getRadialDepthRadius(depth + 1, coreRootCircleRadius, depthSpacing);\n}\n\ntype ContourEdge = { x: number; dotRadius: number };\ntype Contour = { left: ContourEdge; right: ContourEdge }[];\n\n/** Lays `hierarchy` out angularly with each pair of same-depth neighbours just far enough apart for\n * their dots not to overlap on their own ring — a deeper (pop) ring is smaller, so its nodes need\n * more angle each. Leaves each node's `.x` as its angle (rad) from the wedge's leading edge and\n * returns the wedge's minimal span (rad). Without `itemCountRange` every dot is assumed max size.\n *\n * Packs each node's children left to right, each one shifted just far enough to clear the\n * accumulated contour of its earlier siblings, parent centered over its first and last child. Not\n * d3.tree: its Reingold–Tilford pass spreads the push between two wide subtrees evenly across every\n * small sibling between them, which on deep rings leaves leaf siblings far apart. */\nexport function measureWedge(\n _d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n radiusOfDepth: (depth: number) => number,\n dotRadius = NODE_LAYOUT_SLOT_RADIUS,\n): number {\n const slotRadius = () => dotRadius;\n const offsetFromParent = new Map<D3Node, number>();\n const shiftLevel = (level: Contour[number], by: number) => ({\n left: { ...level.left, x: level.left.x + by },\n right: { ...level.right, x: level.right.x + by },\n });\n\n const pack = (node: D3Node): Contour => {\n const own = { x: 0, dotRadius: slotRadius() };\n if (!node.children) return [{ left: own, right: own }];\n const below: Contour = [];\n const childOffsets: number[] = [];\n for (const child of node.children) {\n const contour = pack(child);\n let offset = 0;\n if (childOffsets.length > 0) {\n offset = -Infinity;\n for (let k = 0; k < Math.min(below.length, contour.length); k++) {\n const gap =\n (below[k].right.dotRadius + contour[k].left.dotRadius + MIN_DOT_GAP) / radiusOfDepth(child.depth + k);\n offset = Math.max(offset, below[k].right.x - contour[k].left.x + gap);\n }\n }\n childOffsets.push(offset);\n contour.forEach((level, k) => {\n const shifted = shiftLevel(level, offset);\n if (k < below.length) below[k].right = shifted.right;\n else below.push(shifted);\n });\n }\n const center = (childOffsets[0] + childOffsets[childOffsets.length - 1]) / 2;\n node.children.forEach((child, i) => offsetFromParent.set(child, childOffsets[i] - center));\n return [{ left: own, right: own }, ...below.map((level) => shiftLevel(level, -center))];\n };\n\n pack(hierarchy);\n hierarchy.eachBefore((d) => {\n d.x = d.parent ? d.parent.x! + offsetFromParent.get(d)! : 0;\n });\n let min = Infinity;\n let max = -Infinity;\n hierarchy.each((d) => {\n const inset = (slotRadius() + MIN_DOT_GAP / 2) / radiusOfDepth(d.depth);\n min = Math.min(min, d.x! - inset);\n max = Math.max(max, d.x! + inset);\n });\n hierarchy.each((d) => {\n d.x = d.x! - min;\n });\n return max - min;\n}\n\n// Cap on how far a wedge's measured layout may stretch to use slack in its slice: a uniform angular\n// stretch grows outer rings' arc gaps with their radius, so stretching to fill pushes deep siblings\n// far apart. Unused slack stays as empty gutter around the centered layout.\nexport const MAX_WEDGE_STRETCH = 1.42;\n\n/** What `layoutWedge` does with the angle left over once the layout is stretched to\n * `MAX_WEDGE_STRETCH`: `\"center\"` leaves it as an empty gutter on both sides of the wedge;\n * `\"between-children\"` spreads it into even gaps between the first branching node's children, so\n * the layout reaches both edges of its wedge. */\nexport type WedgeSlack = \"center\" | \"between-children\";\n\nfunction splitAtBranching(hierarchy: D3Node): { chain: D3Node[]; branching: D3Node; children: D3Node[] } | null {\n let branching: D3Node = hierarchy;\n const chain: D3Node[] = [];\n while (branching.children?.length === 1) {\n chain.push(branching);\n branching = branching.children[0];\n }\n const children = branching.children;\n return children && children.length > 1 ? { chain, branching, children } : null;\n}\n\n/** Measures `hierarchy` (see measureWedge), squeezes it to fit `wedgeSpanDegrees` or stretches it\n * by at most `MAX_WEDGE_STRETCH`, places it per `slack`, and projects each node to cartesian x/y\n * around the wheel's center at its depth's radius. */\nexport function layoutWedge(\n d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n wedgeCenterAngleDegrees: number,\n wedgeSpanDegrees: number,\n radiusOfDepth: (depth: number) => number,\n slack: WedgeSlack = \"center\",\n dotRadius = NODE_LAYOUT_SLOT_RADIUS,\n): D3Node {\n const neededRad = measureWedge(d3Lib, hierarchy, radiusOfDepth, dotRadius);\n const wedgeSpanRad = (wedgeSpanDegrees * Math.PI) / 180;\n const scale = Math.min(MAX_WEDGE_STRETCH, wedgeSpanRad / neededRad);\n const wedgeCenterRad = (wedgeCenterAngleDegrees * Math.PI) / 180;\n const centeredStartRad = wedgeCenterRad - (neededRad * scale) / 2;\n\n const branchingSplit = slack === \"between-children\" ? splitAtBranching(hierarchy) : null;\n if (!branchingSplit) {\n hierarchy.each((d) => {\n const angleRad = centeredStartRad + d.x! * scale;\n const radius = radiusOfDepth(d.depth);\n d.x = radius * Math.sin(angleRad);\n d.y = -radius * Math.cos(angleRad);\n });\n return hierarchy;\n }\n\n const { chain, branching, children } = branchingSplit;\n const gap = Math.max(0, wedgeSpanRad - neededRad * scale) / (children.length - 1);\n const wedgeStartRad = wedgeCenterRad - wedgeSpanRad / 2;\n const angleOffsetRad = new Map<D3Node, number>();\n hierarchy.each((d) => angleOffsetRad.set(d, d.x! * scale));\n children.forEach((child, i) => {\n child.each((d) => angleOffsetRad.set(d, angleOffsetRad.get(d)! + i * gap));\n });\n const recenter = (gap * (children.length - 1)) / 2;\n [...chain, branching].forEach((d) => angleOffsetRad.set(d, angleOffsetRad.get(d)! + recenter));\n\n hierarchy.each((d) => {\n const angleRad = wedgeStartRad + angleOffsetRad.get(d)!;\n const radius = radiusOfDepth(d.depth);\n d.x = radius * Math.sin(angleRad);\n d.y = -radius * Math.cos(angleRad);\n });\n return hierarchy;\n}\n\n/**\n * Lays out the center \"Mainstream Pop\" node's own subtree in polar coordinates, full-circle\n * (unlike `computePopRadialLayout`'s 80deg wedge — the center node has no single ring-root\n * direction to anchor to). `hierarchy` must be rooted at the center node itself: depth 0 (the\n * center node) is pinned to the wheel's true origin and excluded from angular layout; depth 1 (its\n * direct children) land exactly one `depthSpacing` step past `coreRootCircleRadius` — the same\n * circle every ring root's pop hierarchy's absolute-depth-1 node lands on too, so the center\n * subtree's rings coincide with the ring roots' own absolute depth circles — spread around the\n * full circle proportional to each child's own subtree size (d3.tree's default separation, unlike\n * the wedge layout's measured one); each deeper generation steps outward by\n * `depthSpacing`.\n */\nexport function computeCenterRadialLayout(\n d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n coreRootCircleRadius: number,\n depthSpacing: number,\n): D3Node {\n const treeLayout = d3Lib.tree<GenreTreeNode>().size([2 * Math.PI, 1]);\n treeLayout(hierarchy);\n\n hierarchy.each((d) => {\n if (d.depth === 0) {\n d.x = 0;\n d.y = 0;\n return;\n }\n const angleRad = d.x!;\n const radius = getRadialDepthRadius(\n d.depth,\n coreRootCircleRadius,\n depthSpacing,\n );\n d.x = radius * Math.sin(angleRad);\n d.y = -radius * Math.cos(angleRad);\n });\n\n return hierarchy;\n}\n\n/** The \"mainstream pop outer circle\" radius (px, from the wheel's center) the center \"Mainstream\n * Pop\" node's own subtree needs to render without overlapping the wheel's own edge (the \"core\n * root circle\") — driven by its deepest node's ring plus that node's own half-width and a fixed\n * outer margin. Only meaningful when the center node actually has children (`hierarchy.height >=\n * 1`). */\nexport function calculateMainstreamPopOuterCircleRadius(\n hierarchy: D3Node,\n coreRootCircleRadius: number,\n depthSpacing: number,\n): number {\n return (\n getRadialDepthRadius(hierarchy.height, coreRootCircleRadius, depthSpacing) +\n MAX_NODE_WIDTH / 2 +\n POP_SUBTREE_OUTER_MARGIN\n );\n}\n\n/** Cartesian (x, y) position on the circle of the given radius, at the given angle in the CSS\n * `rotate()` convention (0 = top, clockwise) — the same convention `computePopRadialLayout` and\n * `computeCoreRadialLayout` project their own nodes with, and the one `.gtv-wheel-slot`'s\n * `rotate(...) translateY(-radius)` (styles.css) positions the root's own JSX-rendered chip with.\n * Used to find where that chip sits in the D3 hierarchy's coordinate space, so the root->depth1\n * link can be drawn even though the root itself isn't part of the hierarchy. */\nexport function getRadialPointOnCircle(\n angleDegrees: number,\n radius: number,\n): { x: number; y: number } {\n const angleRad = (angleDegrees * Math.PI) / 180;\n return { x: radius * Math.sin(angleRad), y: -radius * Math.cos(angleRad) };\n}\n\n// Average glyph advance as a fraction of font size — a DOM-free label width estimate, so layout\n// and collision culling stay deterministic (happy-dom has no getBBox).\nexport const LABEL_CHAR_WIDTH_RATIO = 0.6;\n\nexport interface DotLabelCandidate {\n x: number;\n y: number;\n anchor: \"start\" | \"middle\" | \"end\";\n box: Box;\n}\n\n/** A radial dot node's label placement and boxes, local to the node's own center. `candidates` are\n * the label's possible spots in preference order (see hideCollidingLabels): beside the dot facing\n * away from the wheel's vertical axis (right of the dot when `x >= 0`, left otherwise, so it never\n * reads across the wheel's center), then the other side, then centered above, then below.\n * `labelX`/`anchor`/`label` are the first candidate. With `zoomScale`, the font size is the one that\n * renders at the label's fixed on-screen size under that zoom (see calculateLabelFontSizeAtZoom). */\nexport function getDotLabelGeometry(\n x: number,\n name: string,\n itemCount: number,\n itemCountRange: ItemCountRange,\n zoomScale?: number,\n): {\n dotRadius: number;\n fontSize: number;\n labelX: number;\n anchor: \"start\" | \"end\";\n label: Box;\n dot: Box;\n hitArea: Box;\n candidates: DotLabelCandidate[];\n} {\n const dotRadius = calculateNodeDotRadius(itemCount, itemCountRange);\n const fontSize =\n zoomScale === undefined\n ? calculateNodeFontSize(itemCount, itemCountRange)\n : calculateLabelFontSizeAtZoom(itemCount, itemCountRange, zoomScale);\n const candidates = getLabelCandidates(x, name, dotRadius, fontSize);\n const { x: labelX, box: label } = candidates[0];\n const dot = { x0: -dotRadius, x1: dotRadius, y0: -dotRadius, y1: dotRadius };\n const halfHeight = Math.max(dotRadius, fontSize / 2);\n return {\n dotRadius,\n fontSize,\n labelX,\n anchor: x >= 0 ? \"start\" : \"end\",\n label,\n dot,\n hitArea: {\n x0: Math.min(dot.x0, label.x0),\n x1: Math.max(dot.x1, label.x1),\n y0: -halfHeight,\n y1: halfHeight,\n },\n candidates,\n };\n}\n\nfunction getLabelCandidates(x: number, name: string, dotRadius: number, fontSize: number): DotLabelCandidate[] {\n const width = name.length * fontSize * LABEL_CHAR_WIDTH_RATIO;\n const halfFont = fontSize / 2;\n const side = (right: boolean): DotLabelCandidate => {\n const labelX = (right ? 1 : -1) * (dotRadius + NODE_DOT_LABEL_GAP);\n return {\n x: labelX,\n y: 0,\n anchor: right ? \"start\" : \"end\",\n box: { x0: right ? labelX : labelX - width, x1: right ? labelX + width : labelX, y0: -halfFont, y1: halfFont },\n };\n };\n const stacked = (below: boolean): DotLabelCandidate => {\n const y = (below ? 1 : -1) * (dotRadius + NODE_DOT_LABEL_GAP + halfFont);\n return { x: 0, y, anchor: \"middle\", box: { x0: -width / 2, x1: width / 2, y0: y - halfFont, y1: y + halfFont } };\n };\n const right = x >= 0;\n return [side(right), side(!right), stacked(false), stacked(true)];\n}\n\n/** A root chip's label, drawn in the SVG (see renderRootLabels) so it shares the dot labels' look\n * and collision placement. `x`/`y` are the chip's center, `itemCount` its subtree total. */\nexport interface RootLabelDatum {\n id: string;\n name: string;\n x: number;\n y: number;\n radius: number;\n itemCount: number;\n /** Named even when no candidate spot is free (the wheel's center); other roots hide like dot labels. */\n alwaysShown?: boolean;\n}\n\nexport function renderRootLabels<E extends SVGElement>(\n container: d3.Selection<E, unknown, null, undefined>,\n roots: RootLabelDatum[],\n): void {\n container\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-root-labels\")\n .selectAll<SVGGElement, RootLabelDatum>(\"g.gtv-root-label\")\n .data(roots)\n .join(\"g\")\n .attr(\"class\", \"gtv-root-label\")\n .attr(\"transform\", (d) => `translate(${d.x}, ${d.y})`)\n .append(\"text\")\n .attr(\"class\", \"gtv-node-label\")\n .attr(\"dominant-baseline\", \"middle\")\n .attr(\"fill\", TEXT_COLOR)\n .text((d) => d.name);\n}\n\nexport interface RenderPopSubtreeCallbacks {\n onPlayPause?: (nodeId: string) => void;\n onAddChild?: (parentId: string) => void;\n onRenameRequest?: (node: GenreTreeNode) => void;\n onDeleteRequest?: (node: GenreTreeNode) => void;\n onReparentRequest?: (node: GenreTreeNode) => void;\n onReparentTargetSelect: (newParentId: string) => void;\n onNodeClick?: (node: GenreTreeNode, event: MouseEvent, element: Element) => void;\n onNodeDoubleClick?: (node: GenreTreeNode) => void;\n onNodeHover?: (node: GenreTreeNode) => void;\n additionalActions?: (node: GenreTreeNode) => GenreTreeAction[];\n playingNodeId?: string | null;\n playState?: GenreTreePlayState;\n}\n\n/**\n * Renders an already polar-laid-out pop subtree (see computePopRadialLayout) into `svg`, a `<g>`\n * already translated to the wheel's own center — every node's `.x`/`.y` is that node's true\n * rendered center, unlike tree-renderer.ts's cartesian nodes (which are each node's top-left slot\n * corner, offset by `+ width / 2` at render time). Card drawing, hover toolbar, and reparent-target\n * overlay reuse the exact same NodeHelper/d3-path-helper building blocks tree-renderer.ts's\n * renderTree uses, so pop nodes look and behave identically to every other node in the package —\n * only the positioning math and link shape (radial curves following the wheel's own rings, not\n * tree-renderer's orthogonal links) are specific to this renderer.\n */\nexport function renderPopSubtree(\n d3Lib: typeof import(\"d3\"),\n svg: D3Selection,\n hierarchy: D3Node,\n rootColor: string,\n reparentingNodeId: string | null,\n reparentForbiddenIds: string[],\n callbacks: RenderPopSubtreeCallbacks,\n itemCountRange: ItemCountRange,\n options: {\n skipRootNode?: boolean;\n // Core nodes continue the root chip's own solid-color style (matches every other renderer's\n // treatment of a root); pop nodes keep the lighter tint that sets the pop wedge apart from core.\n isCoreSector?: boolean;\n // Where the hierarchy's own root (depth 0) connects to, when that root ISN'T part of the\n // hierarchy but instead rendered separately as its own JSX wheel chip (every per-root core/pop\n // subtree — buildCoreHierarchy/buildPopHierarchy both exclude the ring root itself, unlike the\n // center \"Mainstream Pop\" subtree, whose hierarchy still includes its own root at depth 0). When\n // given, one extra gtv-link is drawn per depth-0 node from this point to that node, since\n // hierarchy.links() has no edge for it (a hierarchy root has no incoming link).\n rootLinkOrigin?: { x: number; y: number };\n // The center \"Mainstream Pop\" subtree is white: it keeps dark label/toolbar text, grey links and\n // black dot borders; every other pop sector reads light against its tinted fill instead,\n // matching core sectors' solid-color treatment.\n isMainstreamSector?: boolean;\n // The node whose info panel is currently open (see use-node-info-panel.ts) — gets a heavier\n // accent-colored border while every other node in the subtree dims.\n selectedNodeId?: string | null;\n // Per-node fill overriding the sector-wide one derived from rootColor, for a caller whose node\n // colors must not depend on which sector they happen to render in.\n nodeFillOf?: (node: GenreTreeNode) => string;\n // Last rendered dot radius per node id, read to animate a size change (e.g. the size-metric\n // toggle) from the old radius and overwritten with the new ones.\n dotRadiusMemory?: Map<string, number>;\n } = {},\n): void {\n const {\n skipRootNode = false,\n isCoreSector = false,\n rootLinkOrigin,\n isMainstreamSector = false,\n selectedNodeId = null,\n nodeFillOf,\n dotRadiusMemory,\n } = options;\n const { onReparentTargetSelect, onNodeClick, onNodeDoubleClick, onNodeHover } = callbacks;\n const isForbidden = (d: D3Node) => reparentForbiddenIds.includes(d.data.id);\n const isSelected = (d: D3Node) => d.data.id === selectedNodeId;\n // A node is \"related\" to the selected node when it's the selected node's own parent or one of\n // its children — these dim less than the rest of the tree.\n const isRelatedNode = (d: D3Node) =>\n d.parent?.data.id === selectedNodeId ||\n (d.children ?? []).some((c) => c.data.id === selectedNodeId);\n const sectorFill = isCoreSector\n ? rootColor\n : tintSurface(rootColor, POP_SECTOR_TINT_RATIO);\n const nodeFill = (d: D3Node) => nodeFillOf?.(d.data) ?? sectorFill;\n // The center's white rootColor would tint its links and dot borders to white, invisible on the\n // white wheel.\n const linkColor = isMainstreamSector\n ? TEXT_MUTED_COLOR\n : tintSurface(\n rootColor,\n isCoreSector ? RADIAL_CORE_LINK_TINT_RATIO : RADIAL_POP_LINK_TINT_RATIO,\n );\n // skipRootNode omits the hierarchy's own depth-0 node from the drawn cards — used for the center\n // \"Pop\" node's subtree, whose depth-0 node already renders as its own dedicated wheel chip.\n const drawnNodes = skipRootNode\n ? hierarchy.descendants().filter((d) => d.depth > 0)\n : hierarchy.descendants();\n const rootLinks = rootLinkOrigin\n ? hierarchy\n .descendants()\n .filter((d) => d.depth === 0)\n .map((d) => ({\n source: rootLinkOrigin,\n target: { x: d.x!, y: d.y! },\n sourceId: null,\n targetId: d.data.id,\n targetItemCount: d.data.itemCount,\n }))\n : [];\n const links: {\n source: { x?: number; y?: number };\n target: { x?: number; y?: number };\n sourceId: string | null;\n targetId: string | null;\n targetItemCount: number;\n }[] = [\n ...hierarchy.links().map((d) => ({\n source: d.source,\n target: d.target,\n sourceId: d.source.data.id,\n targetId: d.target.data.id,\n targetItemCount: d.target.data.itemCount,\n })),\n ...rootLinks,\n ];\n // A link is \"related\" to the selected node when the selected node is one of its two endpoints\n // (its parent link, or a link to one of its children) — every other link in this subtree dims.\n const isRelatedLink = (d: (typeof links)[number]) =>\n d.sourceId === selectedNodeId || d.targetId === selectedNodeId;\n\n // Cartesian (x, y) here is always a point on a circle centered on the wheel's own center (see\n // computePopRadialLayout/computeCenterRadialLayout), so it's exactly invertible back to the\n // (angle, radius) polar pair d3.linkRadial expects — recovering that lets links curve smoothly\n // along the wheel's rings instead of cutting straight chords across them.\n const toPolar = (p: { x?: number; y?: number }) => {\n const x = p.x ?? 0;\n const y = p.y ?? 0;\n return { x: Math.atan2(x, -y), y: Math.hypot(x, y) };\n };\n const radialLinkGenerator = d3Lib\n .linkRadial<\n { source: { x: number; y: number }; target: { x: number; y: number } },\n { x: number; y: number }\n >()\n .angle((d) => d.x)\n .radius((d) => d.y);\n\n svg\n .selectAll(\"path.gtv-link\")\n .data(links)\n .enter()\n .append(\"path\")\n .attr(\n \"class\",\n (d) =>\n \"gtv-link\" +\n (selectedNodeId && !isRelatedLink(d) ? \" gtv-link--dimmed\" : \"\"),\n )\n .attr(\"d\", (d) =>\n radialLinkGenerator({\n source: toPolar(d.source),\n target: toPolar(d.target),\n }),\n )\n .style(\"fill\", \"none\")\n .style(\"stroke\", linkColor)\n .style(\"stroke-width\", (d) =>\n calculateLinkWidth(d.targetItemCount, itemCountRange),\n )\n .style(\"stroke-linecap\", \"round\");\n\n const nodes = svg\n .selectAll<SVGGElement, D3Node>(\"g.node\")\n .data(drawnNodes)\n .enter()\n .append(\"g\")\n .attr(\n \"class\",\n (d) =>\n \"node\" +\n (isForbidden(d) ? \" gtv-node--forbidden\" : \"\") +\n (selectedNodeId && !isSelected(d)\n ? isRelatedNode(d)\n ? \" gtv-node--dimmed-related\"\n : \" gtv-node--dimmed\"\n : \"\"),\n )\n .attr(\"id\", (d) => \"group-\" + d.data.id)\n // Levels below the nearest root chip (1 = its direct child), for hideCollidingLabels' ranking:\n // with skipRootNode, depth 0 is that chip itself.\n .attr(\"data-gtv-depth\", (d) => d.depth + (skipRootNode ? 0 : 1))\n .attr(\"transform\", (d) => `translate(${d.x}, ${d.y})`)\n .style(\"--gtv-node-fill\", nodeFill);\n\n nodes\n .append(\"rect\")\n .attr(\"class\", \"gtv-hover-hit-area\")\n .each(function (d) {\n const { hitArea } = getDotLabelGeometry(\n d.x!,\n d.data.name,\n d.data.itemCount,\n itemCountRange,\n );\n d3Lib\n .select(this)\n .attr(\"x\", hitArea.x0)\n .attr(\"y\", hitArea.y0)\n .attr(\"width\", hitArea.x1 - hitArea.x0)\n .attr(\"height\", hitArea.y1 - hitArea.y0);\n })\n .attr(\"fill\", \"transparent\")\n .attr(\"pointer-events\", \"all\");\n\n const targetDotRadius = (d: D3Node) => calculateNodeDotRadius(d.data.itemCount, itemCountRange);\n const animateDots =\n dotRadiusMemory !== undefined &&\n !(typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches);\n const startDotRadius = (d: D3Node) =>\n (animateDots ? dotRadiusMemory?.get(d.data.id) : undefined) ?? targetDotRadius(d);\n\n nodes\n .append(\"circle\")\n .attr(\"class\", \"gtv-node-dot\")\n .attr(\"r\", startDotRadius)\n .attr(\"fill\", nodeFill)\n .attr(\"stroke\", (d) =>\n isSelected(d)\n ? ACCENT_COLOR\n : isMainstreamSector\n ? TEXT_COLOR\n : SURFACE_BORDER_COLOR,\n )\n .attr(\"stroke-width\", (d) =>\n isSelected(d)\n ? calculateSelectedNodeBorderWidth(calculateNodeDotRadius(d.data.itemCount, itemCountRange))\n : calculateNodeBorderWidth(d.data.itemCount, itemCountRange),\n )\n .style(\"pointer-events\", \"none\")\n .filter((d) => startDotRadius(d) !== targetDotRadius(d))\n .transition()\n .duration(DOT_RADIUS_TRANSITION_MS)\n .ease(d3Lib.easeCubicOut)\n .attr(\"r\", targetDotRadius);\n\n nodes.each((d) => dotRadiusMemory?.set(d.data.id, targetDotRadius(d)));\n\n // Smallest dots painted last so a big dot never hides a small one it overlaps.\n nodes.sort((a, b) => targetDotRadius(b) - targetDotRadius(a));\n\n nodes\n .append(\"text\")\n .attr(\n \"class\",\n \"gtv-node-label\",\n )\n .each(function (d) {\n const { labelX, anchor, fontSize } = getDotLabelGeometry(\n d.x!,\n d.data.name,\n d.data.itemCount,\n itemCountRange,\n );\n d3Lib\n .select(this)\n .attr(\"x\", labelX)\n .attr(\"text-anchor\", anchor)\n .style(\"font-size\", `${fontSize}px`);\n })\n .attr(\"dominant-baseline\", \"middle\")\n .attr(\"fill\", TEXT_COLOR)\n // Opts back in past .gtv-wheel-pop-layer's inherited pointer-events: none — the hit area is\n // sized for the label's initial spot, and hideCollidingLabels may move the label elsewhere.\n .attr(\"pointer-events\", \"all\")\n .text((d) => d.data.name);\n\n nodes.each(function (d: D3Node) {\n const group = d3Lib.select<SVGGElement, unknown>(this);\n let leaveTimeoutId: ReturnType<typeof setTimeout> | null = null;\n\n group.on(\"mouseenter\", function () {\n if (leaveTimeoutId !== null) {\n clearTimeout(leaveTimeoutId);\n leaveTimeoutId = null;\n }\n\n if (reparentingNodeId && d.data.actionable !== false && !isForbidden(d)) {\n addReparentTargetOverlay(\n d3Lib,\n group as unknown as d3.Selection<\n SVGGElement,\n unknown,\n HTMLElement,\n unknown\n >,\n onReparentTargetSelect,\n itemCountRange,\n );\n }\n });\n\n group.on(\"pointerenter\", () => onNodeHover?.(d.data));\n\n group.on(\"mouseleave\", function () {\n leaveTimeoutId = setTimeout(() => {\n leaveTimeoutId = null;\n d3Lib\n .select<\n SVGGElement,\n unknown\n >(\"#select-as-new-parent-group-\" + d.data.id)\n .remove();\n }, 100);\n });\n\n // Toolbar/menu buttons stopPropagation, so this only fires for a click that actually landed\n // on the card body itself (the reparent-target overlay doesn't stopPropagation, but its click\n // only exists while reparentingNodeId is set, which the guard below already suppresses).\n // See tree-renderer.ts's identical handler.\n const deferredClick = createDeferredClick();\n group.on(\"click\", (event: MouseEvent) => {\n if (reparentingNodeId) return;\n const element = event.currentTarget as Element;\n deferredClick.schedule(\n () => onNodeClick?.(d.data, event, element),\n () => onNodeDoubleClick?.(d.data),\n );\n });\n\n group.on(\"dblclick\", () => deferredClick.doubleClick(() => onNodeDoubleClick?.(d.data)));\n });\n}\n\n/** Places every dot label under `container` (across all rendered subtrees, so labels of\n * neighboring branches/sectors are checked against each other) on its first candidate spot (see\n * getDotLabelGeometry) that overlaps no higher-ranked label (see calculateLabelRank) and no dot, else marks it\n * `gtv-node-label--hidden` (CSS reveals it again on hover). Labels below the zoom's importance\n * threshold (see isLabelRankedForZoom) are hidden outright. Labels are sized for `zoomScale`: zoomed\n * out they are bigger in layout units and collide more, so fewer of them show. */\nexport function hideCollidingLabels<E extends SVGElement>(\n d3Lib: typeof import(\"d3\"),\n container: d3.Selection<E, unknown, null, undefined>,\n itemCountRange: ItemCountRange,\n zoomScale: number,\n): void {\n const nodes = container.selectAll<SVGGElement, D3Node>(\"g.node\");\n const offset = (box: Box, x: number, y: number): Box => ({\n x0: box.x0 + x,\n x1: box.x1 + x,\n y0: box.y0 + y,\n y1: box.y1 + y,\n });\n const geometryById = new Map(\n nodes\n .data()\n .map((d) => [\n d.data.id,\n getDotLabelGeometry(d.x!, d.data.name, d.data.itemCount, itemCountRange, zoomScale),\n ]),\n );\n const roots = container.selectAll<SVGGElement, RootLabelDatum>(\"g.gtv-root-label\");\n const rootGeometryById = new Map(\n roots.data().map((d) => {\n const fontSize = calculateRootLabelFontSize(\n calculateLabelFontSizeAtZoom(d.itemCount, itemCountRange, zoomScale),\n zoomScale,\n );\n return [d.id, { fontSize, candidates: getLabelCandidates(d.x, d.name, d.radius, fontSize) }];\n }),\n );\n const placement = placeLabels([\n ...roots.data().map((d) => ({\n id: `root:${d.id}`,\n labels: rootGeometryById.get(d.id)!.candidates.map((candidate) => offset(candidate.box, d.x, d.y)),\n dot: { x0: d.x - d.radius, x1: d.x + d.radius, y0: d.y - d.radius, y1: d.y + d.radius },\n // Ring roots outrank every dot label (ranks are <= 1), bigger roots first.\n priority: d.alwaysShown ? Infinity : 1 + calculateLabelRank(d.itemCount, itemCountRange, 1),\n forced: d.alwaysShown,\n })),\n ...nodes.nodes().map((element) => {\n const d = d3Lib.select<SVGGElement, D3Node>(element).datum();\n const { candidates, dot } = geometryById.get(d.data.id)!;\n const rank = calculateLabelRank(d.data.itemCount, itemCountRange, Number(element.dataset.gtvDepth));\n return {\n id: d.data.id,\n // An unranked node still keeps its dot as an obstacle for every other label.\n labels: isLabelRankedForZoom(rank, zoomScale)\n ? candidates.map((candidate) => offset(candidate.box, d.x!, d.y!))\n : [],\n dot: offset(dot, d.x!, d.y!),\n priority: rank,\n };\n }),\n ]);\n roots.each(function (d) {\n const { candidates, fontSize } = rootGeometryById.get(d.id)!;\n const index = placement.get(`root:${d.id}`)!;\n const candidate = candidates[Math.max(index, 0)];\n d3Lib\n .select(this)\n .select(\".gtv-node-label\")\n .attr(\"x\", candidate.x)\n .attr(\"y\", candidate.y)\n .attr(\"text-anchor\", candidate.anchor)\n .style(\"font-size\", `${fontSize}px`)\n .classed(\"gtv-node-label--hidden\", index < 0 && !d.alwaysShown);\n });\n nodes.each(function (d) {\n const { candidates, fontSize } = geometryById.get(d.data.id)!;\n const index = placement.get(d.data.id) ?? -1;\n const candidate = candidates[Math.max(index, 0)];\n d3Lib\n .select(this)\n .select(\".gtv-node-label\")\n .attr(\"x\", candidate.x)\n .attr(\"y\", candidate.y)\n .attr(\"text-anchor\", candidate.anchor)\n .style(\"font-size\", `${fontSize}px`)\n .classed(\"gtv-node-label--hidden\", index < 0);\n });\n}\n","import * as d3 from \"d3\";\n\nimport { GenreTreeNode } from \"./types\";\nimport { MAX_NODE_WIDTH, NODE_LAYOUT_SLOT_RADIUS } from \"./constants\";\nimport { buildTreeHierarchyStructure } from \"./NodeHelper\";\nimport { coreRadiusOfDepth, getRadialDepthRadius, layoutWedge } from \"./pop-core-radial-layout\";\n\ntype D3Node = d3.HierarchyNode<GenreTreeNode>;\n\n// Margin (px) added past a core subtree's deepest node's own half-width, so its rendered card\n// never sits flush against the wheel's outer edge — mirrors POP_SUBTREE_OUTER_MARGIN.\nconst CORE_SUBTREE_OUTER_MARGIN = 24;\n\n/** Builds a root's core subtree hierarchy, rooted at the root's own first core child (root not\n * included) — mirrors buildPopHierarchy: that child's own parentId still points at the (excluded)\n * ring root, which d3.stratify would reject, so it's normalized to null here for structure-building\n * only; each resulting hierarchy node's `.data` is then restored to its original (un-normalized)\n * node so the real parentId still reaches renderers/click handlers/the info panel. */\nexport function buildCoreHierarchy(d3Lib: typeof import(\"d3\"), coreNodes: GenreTreeNode[]): D3Node {\n const ids = new Set(coreNodes.map((node) => node.id));\n const byId = new Map(coreNodes.map((node) => [node.id, node]));\n const normalized = coreNodes.map((node) =>\n node.parentId !== null && !ids.has(node.parentId) ? { ...node, parentId: null } : node,\n );\n const hierarchy = buildTreeHierarchyStructure(d3Lib, normalized);\n hierarchy.each((d) => {\n d.data = byId.get(d.data.id) ?? d.data;\n });\n return hierarchy;\n}\n\n/**\n * Lays a root's core (non-pop) subtree out in polar coordinates, fanning outward from\n * `coreRootCircleRadius` within a wedge centered on `wedgeCenterAngleDegrees` — the mirror image of\n * `computePopRadialLayout`'s inward wedge. `hierarchy` is rooted at the ring root's own core\n * child (root not included), so depth 0 (absolute depth 1) lands one `depthSpacing` step past\n * `coreRootCircleRadius`, clear of the root chip's own boundary circle rather than straddling it,\n * and each deeper generation steps further outward by `depthSpacing`, occupying a disjoint radius\n * range from any inward pop wedge in the same quadrant.\n */\nexport function computeCoreRadialLayout(\n d3Lib: typeof import(\"d3\"),\n hierarchy: D3Node,\n wedgeCenterAngleDegrees: number,\n wedgeSpanDegrees: number,\n coreRootCircleRadius: number,\n depthSpacing: number,\n dotRadius: number = NODE_LAYOUT_SLOT_RADIUS,\n): D3Node {\n return layoutWedge(\n d3Lib,\n hierarchy,\n wedgeCenterAngleDegrees,\n wedgeSpanDegrees,\n coreRadiusOfDepth(coreRootCircleRadius, depthSpacing),\n \"between-children\",\n dotRadius,\n );\n}\n\n/** The outer radius (px, past `coreRootCircleRadius`) a core subtree needs to render without\n * overflowing — driven by its deepest node's ring (depth = hierarchy.height, since the hierarchy is\n * rooted at absolute depth 1 already) plus that node's own half-width and a fixed outer margin.\n * Pass 0 for `coreRootCircleRadius` to get the extent as a delta past the ring roots' own circle\n * (used while that circle's own size is still being determined). */\nexport function calculateCoreSubtreeRadialExtent(\n hierarchy: D3Node,\n depthSpacing: number,\n coreRootCircleRadius = 0,\n): number {\n return (\n getRadialDepthRadius(hierarchy.height + 1, coreRootCircleRadius, depthSpacing) +\n MAX_NODE_WIDTH / 2 +\n CORE_SUBTREE_OUTER_MARGIN\n );\n}\n","import { GenreTreeRootGroup } from \"./root-grouping\";\nimport { GenreTreeNode } from \"./types\";\n\nexport interface PopCoreBranch {\n /** The direct child this branch is rooted at. */\n child: GenreTreeNode;\n /** The child and its descendants. */\n nodes: GenreTreeNode[];\n}\n\nexport interface PopCoreSplit {\n /** The root plus every core (non-pop) branch's nodes, flattened. */\n coreNodes: GenreTreeNode[];\n /** Every pop branch's nodes, flattened (root not included). Empty when the root has no pop\n * side (e.g. classical). */\n popNodes: GenreTreeNode[];\n /** One entry per direct core (non-pop) child, each paired with its own subtree. Order matches\n * the child's position among the root's direct children. Empty when the root has no core\n * children. */\n coreBranches: PopCoreBranch[];\n /** One entry per direct pop child, mirroring `coreBranches`. Empty when the root has no pop\n * children. */\n popBranches: PopCoreBranch[];\n}\n\n/**\n * Splits one root group's nodes into its core and pop branches, per the root's direct children's\n * `side` field (\"pop\" for the optional branch; unset/\"core\" for the required one).\n *\n * A root may have any number of core (non-pop) direct children and any number of pop direct\n * children — each becomes its own branch in `coreBranches`/`popBranches`. A root with no pop\n * children yields an empty `popBranches`/`popNodes`. A root with zero direct children yields\n * both `coreBranches` and `popBranches` empty (`coreNodes` is just the root).\n */\nexport function splitRootGroupBySide(group: GenreTreeRootGroup): PopCoreSplit {\n const { root, nodes } = group;\n const nodeById = new Map(nodes.map((node) => [node.id, node]));\n const directChildren = nodes.filter((node) => node.parentId === root.id);\n\n const popChildren = directChildren.filter((node) => node.side === \"pop\");\n const coreChildren = directChildren.filter((node) => node.side !== \"pop\");\n\n const childrenByParentId = new Map<string, GenreTreeNode[]>();\n for (const node of nodes) {\n if (node.parentId === null) continue;\n const siblings = childrenByParentId.get(node.parentId);\n if (siblings) siblings.push(node);\n else childrenByParentId.set(node.parentId, [node]);\n }\n\n const collectSubtree = (startId: string): GenreTreeNode[] => {\n const start = nodeById.get(startId)!;\n\n const subtree: GenreTreeNode[] = [];\n const stack = [start];\n while (stack.length > 0) {\n const current = stack.pop()!;\n subtree.push(current);\n stack.push(...(childrenByParentId.get(current.id) ?? []));\n }\n return subtree;\n };\n\n const coreBranches = coreChildren.map((child) => ({ child, nodes: collectSubtree(child.id) }));\n const popBranches = popChildren.map((child) => ({ child, nodes: collectSubtree(child.id) }));\n\n const coreNodes = [root, ...coreBranches.flatMap((branch) => branch.nodes)];\n const popNodes = popBranches.flatMap((branch) => branch.nodes);\n\n return { coreNodes, popNodes, coreBranches, popBranches };\n}\n","\"use client\";\n\nimport { WheelRadialCore } from \"./GenreTreeWheelRadialBase\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\n\nexport interface GenreTreeWheelRadialProps extends Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> {\n nodes: GenreTreeNode[];\n /** Fired whenever the top (just-clicked) root changes — on mount with the default selection,\n * and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** Optional label centered on the wheel's pivot point, e.g. a brand name. */\n centerLabel?: string;\n}\n\n/**\n * All root genres distributed around a full circle centered in its container, each root's\n * angular width proportional to its own subtree's node count, every root developed as a full\n * subtree simultaneously. Clicking any chip re-lays-out the ring so that root lands on the\n * right, recalculating every other root's angle.\n */\nexport function GenreTreeWheelRadial(props: GenreTreeWheelRadialProps) {\n return <WheelRadialCore {...props} />;\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as d3 from \"d3\";\n\nimport { createDeferredClick } from \"./defer-click\";\nimport { buildTreeHierarchyStructure } from \"./NodeHelper\";\nimport { GenreTreeRootGroup, groupNodesByRoot } from \"./root-grouping\";\nimport { useTreeIndex } from \"./use-tree-index\";\nimport { splitRootGroupBySide } from \"./pop-core-split\";\nimport {\n buildCoreHierarchy,\n calculateCoreSubtreeRadialExtent,\n computeCoreRadialLayout,\n} from \"./core-radial-layout\";\nimport {\n buildPopHierarchy,\n calculateMainstreamPopOuterCircleRadius,\n calculatePopSubtreeRadialExtent,\n computeCenterRadialLayout,\n computePopRadialLayout,\n coreRadiusOfDepth,\n getRadialPointOnCircle,\n hideCollidingLabels,\n LABEL_CHAR_WIDTH_RATIO,\n renderRootLabels,\n measureWedge,\n popRadiusOfDepth,\n POP_SUBTREE_OUTER_MARGIN,\n POP_WEDGE_SPAN_DEGREES,\n renderPopSubtree,\n type RootLabelDatum,\n} from \"./pop-core-radial-layout\";\nimport {\n buildSectorClipPathPolygon,\n computeRadialLayout,\n computeSectorWidths,\n findSectorAtAngle,\n pickEmptyLabelSpot,\n RadialSlot,\n solveRingRadius,\n subdivideWedge,\n} from \"./radial-wheel-geometry\";\nimport { FloatingControls } from \"./FloatingControls\";\nimport { usePanZoom } from \"./use-pan-zoom\";\nimport { queryTreeContentElements } from \"./zoom-pan\";\nimport { useNodeInfoPanel } from \"./use-node-info-panel\";\nimport { useFocusHistory } from \"./use-focus-history\";\nimport { WheelBreadcrumb } from \"./WheelBreadcrumb\";\nimport { resolveInfoPanelObscuredArea } from \"./info-panel-geometry\";\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\nimport { isZoomableNode, resolvePopCoreFrame } from \"./pop-core-frame\";\nimport { GenreTreeNode, GenreTreeProps } from \"./types\";\nimport {\n ACCENT_COLOR,\n calculateNodeDotRadius,\n calculateSelectedNodeBorderWidth,\n CENTER_NODE_NAME,\n getGenreNodeColor,\n getGenreNodeStyle,\n getItemCountRange,\n hexToRgba,\n INFO_PANEL_WIDTH,\n isMainstreamPopLineage,\n NODE_DOT_MAX_RADIUS,\n NODE_LAYOUT_SLOT_RADIUS,\n POP_TREE_DEPTH_RADIAL_SPACING,\n ROOT_SECTOR_FILL_OPACITY,\n LABEL_RELAYOUT_DEBOUNCE_MS,\n REGION_LABEL_MAX_PX,\n REGION_LABEL_MIN_PX,\n REGION_LABEL_TARGET_PX,\n WHEEL_POP_CORE_RADIUS,\n WHEEL_ROTATION_EASING,\n WHEEL_ROTATION_TRANSITION_MS,\n ZOOM_FOCUS_SCALE,\n shadeSurface,\n UNDERGROUND_DARKEN_RATIO,\n UNDERGROUND_SECTOR_FILL_OPACITY,\n SCENE_SECTOR_OUTER_RADIUS,\n} from \"./constants\";\n\n/** Why `onFocusChange` fired: the info panel's \"Zoom in\" button or a node double-click, the parent (\"back\") /\n * root-view buttons and breadcrumb, a `selectedNodeId` outside the current zoom (e.g. a search result), or a\n * `focusNodeId` that isn't zoomable (see `isZoomableNode`). */\nexport type FocusChangeReason = \"zoom-in\" | /** @deprecated no longer emitted */ \"previous\" | \"back\" | \"root\" | \"search\" | \"rejected\";\n\n/** Focus is controlled-only: pass both props or neither, since the wheel never moves its own focus. */\nexport type PopCoreFocusProps =\n | { focusNodeId?: undefined; onFocusChange?: undefined }\n | {\n /** Renders this node's subtree as its own wheel instead of the whole tree (null) — its single\n * pop child (if any) becomes the center disc, its other children the ring roots. An id that\n * isn't zoomable (see `isZoomableNode`) renders the whole tree and fires\n * `onFocusChange(null, \"rejected\")` once. */\n focusNodeId: string | null;\n /** Fired with the node to focus and why: any zoomable node from its info panel's \"Zoom in\"\n * or a double-click on it or its label (`\"zoom-in\"`), the focus node's parent (`\"back\"`), null for the whole tree (`\"root\"`),\n * the selection's top-level root — or null when that root is Mainstream Pop or the selection\n * is itself a root — when `selectedNodeId` lies outside the zoom (`\"search\"`), and null when\n * `focusNodeId` isn't zoomable (`\"rejected\"`). Omit both props to hide the zoom-in and back\n * controls and never refocus on an outside selection. */\n onFocusChange: (id: string | null, reason: FocusChangeReason) => void;\n };\n\nexport type WheelRadialPopCoreProps = Omit<GenreTreeProps, \"nodes\" | \"rootColor\" | \"orientation\"> & {\n nodes: GenreTreeNode[];\n /** Fired whenever the top (just-clicked) root changes — on mount with the default selection,\n * and on every chip click. */\n onRootSelect?: (rootId: string) => void;\n /** When false, clicking a chip still fires `onRootSelect`, but the ring stays at its current\n * rotation instead of bringing the clicked root to the landing angle. Defaults to true. */\n allowWheelRotation?: boolean;\n} & PopCoreFocusProps;\n\n// The wheel always lands the just-clicked root on the right (matches WheelRadialCore's own\n// landingAngle=90 convention) — see computeRadialLayout's doc comment for why.\nconst LANDING_ANGLE = 90;\n\n// Clearance (px) between neighbouring ring root chips along the ring.\nconst CHIP_GAP = 16;\n\nconst sum = (values: number[]) => values.reduce((total, value) => total + value, 0);\n\n// Unwraps computeRadialLayout's [0, 360)-wrapped angles into each root's own continuous\n// (possibly >360 or negative) angle, nearest to its `previous` value — see WheelRadialCore's own\n// copy of this function for why plain wrapped angles cause a wrong-direction transition.\nfunction computeContinuousAngles(\n groups: GenreTreeRootGroup[],\n layout: RadialSlot[],\n previous: Map<string, number>,\n): Map<string, number> {\n const angles = new Map<string, number>();\n groups.forEach((group, index) => {\n const rawAngle = layout[index]?.angle ?? 0;\n const previousAngle = previous.get(group.root.id);\n const laps =\n previousAngle === undefined\n ? 0\n : Math.round((previousAngle - rawAngle) / 360);\n angles.set(group.root.id, rawAngle + 360 * laps);\n });\n return angles;\n}\n\n/**\n * Same ring mechanics as `WheelRadialCore`, with two differences: every root's outward subtree\n * renders only its *core* (non-pop) branch (`side !== \"pop\"`), and — if that root also has a pop\n * branch — that pop subtree renders as a full interactive tree fanned out *inside* the wheel's own\n * circle, in the root's own sector, instead of being hidden. The circle grows past its normal\n * chip-clearance floor as needed to fit the largest pop subtree (see\n * `calculatePopSubtreeRadialExtent`).\n */\nexport function WheelRadialPopCoreCore({\n nodes,\n className,\n onRootSelect,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n onNodeHover,\n additionalActions,\n showToolbar = true,\n renderInfoPanelBody,\n onSelectedNodeChange,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n itemCountScaleMax,\n nodeCountForSizing,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n wheelZoom,\n defaultViewport,\n onViewportChange,\n controlsOrientation = \"vertical\",\n showZoomControls = \"auto\",\n floatingControlsLeading,\n busy,\n allowWheelRotation = true,\n selectedNodeId,\n focusNodeId: requestedFocusNodeId = null,\n onFocusChange,\n}: WheelRadialPopCoreProps) {\n const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;\n const index = useTreeIndex(nodes);\n // A focus the zoom buttons couldn't have produced (stale URL, deleted node, ...) renders the\n // unfocused wheel and asks the consumer to reset it, instead of letting resolvePopCoreFrame throw.\n const focusNodeId = useMemo(\n () => (requestedFocusNodeId !== null && isZoomableNode(nodes, requestedFocusNodeId) ? requestedFocusNodeId : null),\n [nodes, requestedFocusNodeId],\n );\n const isFocusRejected = requestedFocusNodeId !== null && focusNodeId === null;\n useEffect(() => {\n if (isFocusRejected) onFocusChange?.(null, \"rejected\");\n // eslint-disable-next-line react-hooks/exhaustive-deps -- report each rejected id once, not on every new callback identity\n }, [isFocusRejected, requestedFocusNodeId]);\n const { center: layoutCenterNode, centerHasPopDisc, centerNodes: centerSubtreeNodes, ringNodes } = useMemo(\n () => resolvePopCoreFrame(nodes, focusNodeId),\n [nodes, focusNodeId],\n );\n // The frame rewrites a focused view's center/ring roots to parentId: null for layout only —\n // anything handed to the consumer or the info panel gets the original node instead.\n const centerNode = index.byId.get(layoutCenterNode.id)!;\n // A parent with several pop children can't be a focus, so Root view is the only way up from there.\n\n const centerSubtreeNodeIds = useMemo(\n () => new Set(centerSubtreeNodes.map((node) => node.id)),\n [centerSubtreeNodes],\n );\n\n // computeCenterRadialLayout spreads each depth-1 child's wedge proportional to its own subtree\n // size (d3.tree's default separation) — feeding it the center node's pop-side children too would\n // let their descendant counts inflate their core siblings' neighboring wedges, so they're\n // excluded here the same way splitRootGroupBySide excludes a ring root's pop branch from its core\n // one. Unlike the ring roots, the center node's own pop branch (if any) has no dedicated wedge\n // rendering path today, so excluding it here also drops it from view entirely.\n const centerCoreSubtreeNodes = useMemo(() => {\n if (!centerHasPopDisc) return [layoutCenterNode];\n const { childrenByParentId } = index;\n const coreChildren = (childrenByParentId.get(layoutCenterNode.id) ?? []).filter(\n (child) => child.side !== \"pop\",\n );\n const subtree: GenreTreeNode[] = [layoutCenterNode];\n const stack = [...coreChildren];\n while (stack.length > 0) {\n const current = stack.pop()!;\n subtree.push(current);\n stack.push(...(childrenByParentId.get(current.id) ?? []));\n }\n return subtree;\n }, [index, layoutCenterNode, centerHasPopDisc]);\n\n const centerSubtreeHierarchy = useMemo(\n () =>\n centerCoreSubtreeNodes.length > 1\n ? buildTreeHierarchyStructure(d3, centerCoreSubtreeNodes)\n : null,\n [centerCoreSubtreeNodes],\n );\n // The center \"Mainstream Pop\" node's own subtree is always shown when it has one; without one the\n // center chip stays a plain disc.\n const isPopExpanded = centerSubtreeHierarchy != null;\n\n const groups = useMemo(() => groupNodesByRoot(ringNodes), [ringNodes]);\n\n const splitByRootId = useMemo(\n () =>\n new Map(\n groups.map((group) => [group.root.id, splitRootGroupBySide(group)]),\n ),\n [groups],\n );\n const ringRootById = useMemo(\n () => new Map(groups.flatMap((group) => group.nodes.map((node) => [node.id, group.root]))),\n [groups],\n );\n // Every chip, dot and InfoPanel header takes this, so a genre looks the same in every zoom frame.\n const getNodeVisualStyle = useCallback(\n (node: GenreTreeNode) => getGenreNodeStyle(index, node.id),\n [index],\n );\n\n const [topRootId, setTopRootId] = useState<string | null>(\n groups[0]?.root.id ?? null,\n );\n // Tracks which root the ring is actually rotated to show at the landing angle — kept separate\n // from topRootId so a click can still fire onRootSelect (via topRootId) without moving the ring\n // when allowWheelRotation is false. Only updated on click when allowWheelRotation is true, so it\n // can lag behind topRootId until the next click after rotation is re-enabled.\n const [rotationTopRootId, setRotationTopRootId] = useState<string | null>(\n groups[0]?.root.id ?? null,\n );\n const [renderedFocusNodeId, setRenderedFocusNodeId] = useState(focusNodeId);\n if (renderedFocusNodeId !== focusNodeId) {\n setRenderedFocusNodeId(focusNodeId);\n setTopRootId(groups[0]?.root.id ?? null);\n setRotationTopRootId(groups[0]?.root.id ?? null);\n }\n const viewportRef = useRef<HTMLDivElement>(null);\n const stageRef = useRef<HTMLDivElement>(null);\n const dotRadiusMemoryRef = useRef(new Map<string, number>());\n const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });\n // Shared across every mounted sector (ring roots' pop/core branches, and the center subtree) —\n // exactly one hook instance for the whole component instance, so only one panel is ever open at\n // a time regardless of which sector's node was clicked.\n const { panel: openPanel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });\n // Derived per frame, not closed by an effect: a focus change can leave the open (or selected)\n // node outside the drawn subtree, and nothing orders such an effect against the selection one.\n // The focus node itself counts: when it has a pop child that child is the drawn center, so the\n // node zoomed into from its panel isn't drawn but its panel should stay open.\n const isInFrame = (id: string) => id === focusNodeId || centerSubtreeNodeIds.has(id) || ringRootById.has(id);\n const panel = openPanel && isInFrame(openPanel.node.id) ? openPanel : null;\n const { previous: previousSelectedId, markPreviousRequested } = useFocusHistory(\n panel?.node.id ?? null,\n isInFrame,\n );\n const selectNodeFromPanel = (id: string) => {\n const targetNode = index.byId.get(id)!;\n const element = viewportRef.current!.querySelector(`#group-${CSS.escape(id)}`);\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(targetNode, element ?? viewportRef.current, viewportRef.current);\n };\n // Read via a ref inside the D3 render effect below rather than depending on `panZoom` directly —\n // its identity changes every render, which would otherwise re-run (and re-mount the whole D3\n // tree) on every pan/zoom, exactly what that effect's own dependency list is designed to avoid.\n const centerOnElementRef = useRef(panZoom.centerOnElement);\n useEffect(() => {\n centerOnElementRef.current = panZoom.centerOnElement;\n });\n const showNodeInfoRef = useRef(showNodeInfo);\n useEffect(() => {\n showNodeInfoRef.current = showNodeInfo;\n });\n // Double-click mirrors the info panel's \"Zoom in\" button.\n const chipClickRef = useRef(createDeferredClick());\n useEffect(() => {\n const deferred = chipClickRef.current;\n return deferred.cancel;\n }, []);\n const focusOnNode = (id: string) => {\n chipClickRef.current.cancel();\n if (!onFocusChange || reparentingNodeId || id === focusNodeId || !isZoomableNode(nodes, id)) return;\n closeNodeInfo();\n onFocusChange(id, \"zoom-in\");\n };\n const focusOnNodeRef = useRef(focusOnNode);\n useEffect(() => {\n focusOnNodeRef.current = focusOnNode;\n });\n // Read via a ref so a consumer's fresh closure each render doesn't re-mount the D3 tree.\n const onNodeHoverRef = useRef(onNodeHover);\n useEffect(() => {\n onNodeHoverRef.current = onNodeHover;\n });\n const { zoomScale } = panZoom;\n const zoomScaleRef = useRef(zoomScale);\n useEffect(() => {\n zoomScaleRef.current = zoomScale;\n });\n const wheelCircleRef = useRef<HTMLDivElement>(null);\n const popSvgRef = useRef<SVGSVGElement>(null);\n const centerChipRef = useRef<HTMLButtonElement>(null);\n const regionLabelRef = useRef<HTMLDivElement>(null);\n // Stage-space (pre pan/zoom) position and font size of the current root's name, so it pans and\n // scales with the tree between re-placements.\n const [regionLabel, setRegionLabel] = useState<{\n rootId: string;\n x: number;\n y: number;\n fontSize: number;\n } | null>(null);\n\n // Falls back to the first root without writing state back when the explicitly selected root\n // disappears from `nodes` — avoids a setState-in-effect cascading render for derived state.\n const effectiveTopRootId = groups.some((group) => group.root.id === topRootId)\n ? topRootId\n : (groups[0]?.root.id ?? null);\n const effectiveRotationTopRootId = groups.some(\n (group) => group.root.id === rotationTopRootId,\n )\n ? rotationTopRootId\n : (groups[0]?.root.id ?? null);\n const topIndex = Math.max(\n groups.findIndex((group) => group.root.id === effectiveRotationTopRootId),\n 0,\n );\n\n const rootItemCountById = useMemo(\n () => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),\n [groups],\n );\n\n // Pop-subtree node cards scale against the whole tree's item counts, not just their own subtree's\n // — a pop chain's few nodes can have a narrow itemCount spread that would otherwise get stretched\n // across the full size range and render wildly inconsistent card sizes next to core siblings.\n // Root counts included so root circles and node dots share one track-count scale.\n const wheelItemCountRange = useMemo(\n () =>\n getItemCountRange([\n ...nodes,\n ...[...rootItemCountById.values()].map((itemCount) => ({ itemCount })),\n ], { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),\n [nodes, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing],\n );\n\n const isCenterMainstream = isMainstreamPopLineage(index, centerNode.id);\n const centerNodeColor = isCenterMainstream ? \"#ffffff\" : getGenreNodeStyle(index, centerNode.id).fill;\n const centerChipDiameter = 2 * calculateNodeDotRadius(centerNode.itemCount, wheelItemCountRange);\n\n const branchHierarchiesByRootId = useMemo(\n () =>\n new Map(\n groups.map((group) => {\n const split = splitByRootId.get(group.root.id);\n return [\n group.root.id,\n {\n pop: (split?.popBranches ?? []).map((branch) => buildPopHierarchy(d3, branch.nodes)),\n core: (split?.coreBranches ?? []).map((branch) => buildCoreHierarchy(d3, branch.nodes)),\n },\n ];\n }),\n ),\n [groups, splitByRootId],\n );\n\n // How far past the ring roots' own circle the tallest developed root's pop wedge reaches —\n // computed as a delta (base radius 0) since the actual base (coreRootCircleRadius) isn't known\n // yet; folded into coreRootCircleRadius below, then re-applied at the real base for rendering.\n const maxPopExtentDelta = useMemo(() => {\n let extent = 0;\n branchHierarchiesByRootId.forEach(({ pop }) => {\n pop.forEach((hierarchy) => {\n extent = Math.max(extent, calculatePopSubtreeRadialExtent(hierarchy, 0));\n });\n });\n return extent;\n }, [branchHierarchiesByRootId]);\n\n // The mainstream circle's radius from the wheel's true center — how far the center node's own\n // subtree reaches, or the center chip's own disc when it has none. The deepest pop node must\n // clear this, not just some fixed distance past the ring roots' own circle.\n const mainstreamCircleRadius = useMemo(\n () =>\n centerSubtreeHierarchy\n ? calculateMainstreamPopOuterCircleRadius(\n centerSubtreeHierarchy,\n 0,\n POP_TREE_DEPTH_RADIAL_SPACING,\n )\n : wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS,\n [centerSubtreeHierarchy, wheelItemCountRange.maxDotRadius],\n );\n\n // How far past the ring roots' own circle the deepest developed root's core branch reaches —\n // same idea as maxPopExtentDelta. Core branches render outward from coreRootCircleRadius\n // regardless of its value, so this doesn't belong in coreRootCircleRadius itself (that would\n // needlessly drag the visual outer circle and ring root chips outward with it) — it only feeds\n // svgCanvasRadius below, so the deepest core branch has enough SVG canvas to render into\n // without being clipped.\n const maxCoreExtentDelta = useMemo(() => {\n let extent = 0;\n branchHierarchiesByRootId.forEach(({ core }) => {\n core.forEach((hierarchy) => {\n extent = Math.max(\n extent,\n calculateCoreSubtreeRadialExtent(hierarchy, POP_TREE_DEPTH_RADIAL_SPACING, 0),\n );\n });\n });\n return extent;\n }, [branchHierarchiesByRootId]);\n\n // Pins coreRootCircleRadius so the deepest node of the deepest developed pop branch (which\n // climbs inward from coreRootCircleRadius by a fixed depthSpacing per depth step — see\n // computePopRadialLayout) has its card clear the mainstream circle by the usual half-width +\n // outer margin, rather than leaving that gap to whatever coreRootCircleRadius happens to be for\n // other reasons. Zero (dropped from the Math.max below) when no root has a pop branch at all.\n const popReachRequiredRadius = useMemo(\n () =>\n maxPopExtentDelta > 0 ? mainstreamCircleRadius + maxPopExtentDelta : 0,\n [mainstreamCircleRadius, maxPopExtentDelta],\n );\n\n // Angles (rad) each root's pop branches, core branches and chip need at ring radius `radius`.\n const measureBranchNeedsAt = useCallback(\n (radius: number) =>\n new Map(\n groups.map((group) => {\n const { pop, core } = branchHierarchiesByRootId.get(group.root.id)!;\n const dotRadius = wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS;\n const chipWidth = 2 * dotRadius;\n return [\n group.root.id,\n {\n pop: pop.map((hierarchy) =>\n measureWedge(\n d3,\n hierarchy,\n popRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),\n dotRadius,\n ),\n ),\n core: core.map((hierarchy) =>\n measureWedge(\n d3,\n hierarchy,\n coreRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),\n dotRadius,\n ),\n ),\n chip: 2 * Math.asin(Math.min(1, (chipWidth / 2 + CHIP_GAP) / radius)),\n },\n ];\n }),\n ),\n [groups, branchHierarchiesByRootId, wheelItemCountRange.maxDotRadius],\n );\n\n // The visual outer circle's radius (--gtv-wheel-radius) and, equally, where every ring root's\n // own chip sits — NOT the SVG canvas size (see svgCanvasRadius below). The smallest radius at\n // which every root's measured need fits the full circle, floored so ring root chips clear the\n // mainstream circle and the deepest pop branch clears it too. Deliberately excludes\n // maxCoreExtentDelta: a deep core branch needs canvas room, not a bigger ring.\n const coreRootCircleRadius = useMemo(\n () =>\n solveRingRadius(\n (radius) =>\n [...measureBranchNeedsAt(radius).values()].reduce(\n (total, needs) => total + Math.max(sum(needs.pop), sum(needs.core), needs.chip),\n 0,\n ),\n Math.max(\n WHEEL_POP_CORE_RADIUS,\n mainstreamCircleRadius + (wheelItemCountRange.maxDotRadius ?? NODE_DOT_MAX_RADIUS) + POP_SUBTREE_OUTER_MARGIN,\n popReachRequiredRadius,\n ),\n ),\n [measureBranchNeedsAt, mainstreamCircleRadius, popReachRequiredRadius, wheelItemCountRange.maxDotRadius],\n );\n\n const branchNeedsByRootId = useMemo(\n () => measureBranchNeedsAt(coreRootCircleRadius),\n [measureBranchNeedsAt, coreRootCircleRadius],\n );\n\n // Each root's sector is the angle it measurably needs at the solved ring radius — the widest of\n // its pop wedge, its core wedge (disjoint radius bands) and its own chip — so slack is shared in\n // proportion to real need rather than node count.\n const rootWeights = useMemo(\n () =>\n groups.map((group) => {\n const needs = branchNeedsByRootId.get(group.root.id)!;\n return Math.max(sum(needs.pop), sum(needs.core), needs.chip);\n }),\n [groups, branchNeedsByRootId],\n );\n\n const layout = useMemo(\n () => computeRadialLayout(rootWeights, topIndex, LANDING_ANGLE),\n [rootWeights, topIndex],\n );\n\n // computeRadialLayout always returns angles wrapped to [0, 360) — re-rendering with a fresh\n // wrapped value on every selection change means a chip landing back near 0deg (top) has to\n // transition from e.g. 270deg down through 90/0, i.e. always counterclockwise, regardless of\n // which way the ring conceptually just turned. Track each chip's own last displayed (unwrapped)\n // angle and add/subtract full turns so its CSS transition always takes its own shortest path.\n const [angleMemo, setAngleMemo] = useState(() => ({\n layout,\n angles: computeContinuousAngles(groups, layout, new Map()),\n }));\n if (angleMemo.layout !== layout) {\n setAngleMemo({\n layout,\n angles: computeContinuousAngles(groups, layout, angleMemo.angles),\n });\n }\n const continuousAngleByRootId = angleMemo.angles;\n\n // Real angular sector each ring root owns, proportional to its own weight (rootWeights) out of\n // the total — same widths computeRadialLayout placed chips with, so a root's pop/core wedge below\n // is guaranteed to fit within its actual sector without spilling into a neighbor's.\n const sectorSpanByRootId = useMemo(() => {\n const map = new Map<string, number>();\n if (groups.length <= 1) return map;\n const widths = computeSectorWidths(rootWeights);\n groups.forEach((group, index) => {\n map.set(group.root.id, widths[index]);\n });\n return map;\n }, [groups, rootWeights]);\n\n const wedgeSpanForRoot = useCallback(\n (rootId: string) => sectorSpanByRootId.get(rootId) ?? POP_WEDGE_SPAN_DEGREES,\n [sectorSpanByRootId],\n );\n\n // Each root's own single angle on the ring — unlike a branch's sub-wedge angle (which fans that\n // branch's subtree out at its own position within the root's wedge), a link from the root down\n // to any of its branches must originate from the root's own unique chip position.\n const rootAngleByRootId = useMemo(() => {\n const map = new Map<string, number>();\n groups.forEach((group, index) => {\n map.set(group.root.id, layout[index]?.angle ?? 0);\n });\n return map;\n }, [groups, layout]);\n\n // A root's wedge is subdivided across its own pop (resp. core) branches by each branch's\n // measured need — mirrors how ring roots themselves share the full circle.\n const placeBranches = useCallback(\n (side: \"pop\" | \"core\") => {\n const map = new Map<\n string,\n { hierarchy: d3.HierarchyNode<GenreTreeNode>; angle: number; span: number }[]\n >();\n groups.forEach((group) => {\n const hierarchies = branchHierarchiesByRootId.get(group.root.id)![side];\n if (hierarchies.length === 0) return;\n const slices = subdivideWedge(\n rootAngleByRootId.get(group.root.id)!,\n wedgeSpanForRoot(group.root.id),\n branchNeedsByRootId.get(group.root.id)![side],\n );\n map.set(\n group.root.id,\n hierarchies.map((hierarchy, branchIndex) => ({\n hierarchy,\n angle: slices[branchIndex]!.centerDeg,\n span: slices[branchIndex]!.spanDeg,\n })),\n );\n });\n return map;\n },\n [groups, branchHierarchiesByRootId, branchNeedsByRootId, rootAngleByRootId, wedgeSpanForRoot],\n );\n const popHierarchyByRootId = useMemo(() => placeBranches(\"pop\"), [placeBranches]);\n const coreHierarchyByRootId = useMemo(() => placeBranches(\"core\"), [placeBranches]);\n\n // The SVG canvas's actual radius. Core branches render outward from the real coreRootCircleRadius\n // (see computeCoreRadialLayout's call below), so the canvas must extend maxCoreExtentDelta past\n // that actual base — not past the ring-radius floor, which can be smaller than coreRootCircleRadius\n // whenever pop reach or the center subtree is what's sizing it — or the deepest core\n // node gets clipped. Purely a rendering-surface concern: never feeds back into coreRootCircleRadius,\n // so it doesn't affect the visual outer circle or ring root chip placement.\n const svgCanvasRadius = useMemo(\n () => coreRootCircleRadius + maxCoreExtentDelta,\n [coreRootCircleRadius, maxCoreExtentDelta],\n );\n\n // Boundary the center Mainstream Pop node's subtree currently occupies, drawn as a cosmetic\n // marker — the subtree itself renders inside this circle (see computeCenterRadialLayout below),\n // while every root's pop wedges fan outward from it toward coreRootCircleRadius.\n const middleCircleFloor = mainstreamCircleRadius;\n\n // Read via a ref rather than depending on `onRootSelect` directly — consumers commonly pass an\n // inline callback, which would otherwise re-fire this effect (and any state it sets) every render.\n const onRootSelectRef = useRef(onRootSelect);\n useEffect(() => {\n onRootSelectRef.current = onRootSelect;\n });\n\n useEffect(() => {\n if (effectiveTopRootId) onRootSelectRef.current?.(effectiveTopRootId);\n }, [effectiveTopRootId]);\n\n useEffect(() => {\n if (!popSvgRef.current) return;\n\n const svg = d3.select(popSvgRef.current);\n svg.selectAll(\"*\").remove();\n const originGroup = svg\n .append(\"g\")\n .attr(\"transform\", `translate(${svgCanvasRadius}, ${svgCanvasRadius})`);\n\n popHierarchyByRootId.forEach((branches, rootId) => {\n const rootLinkOrigin = getRadialPointOnCircle(\n rootAngleByRootId.get(rootId) ?? 0,\n coreRootCircleRadius,\n );\n\n const sectorGroup = originGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-pop-sector\")\n .attr(\"data-gtv-root-id\", rootId);\n\n branches.forEach(({ hierarchy, angle, span }, branchIndex) => {\n const laidOut = computePopRadialLayout(\n d3,\n hierarchy,\n angle,\n coreRootCircleRadius,\n span,\n POP_TREE_DEPTH_RADIAL_SPACING,\n wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS,\n );\n const reparentForbiddenIds = reparentingNodeId\n ? (laidOut\n .descendants()\n .find((d) => d.data.id === reparentingNodeId)\n ?.descendants()\n .map((d) => d.data.id) ?? [])\n : [];\n\n const branchGroup = sectorGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-pop-branch\")\n .attr(\"data-gtv-branch-index\", branchIndex);\n\n renderPopSubtree(\n d3,\n branchGroup,\n laidOut,\n getGenreNodeColor(index, rootId),\n reparentingNodeId,\n reparentForbiddenIds,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId)\n void onReparent?.(reparentingNodeId, newParentId);\n },\n onNodeClick: (data, event, clickedElement) => {\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n onNodeDoubleClick: (data) => focusOnNodeRef.current(data.id),\n onNodeHover: (data) => onNodeHoverRef.current?.(data),\n additionalActions,\n playingNodeId,\n playState,\n },\n wheelItemCountRange,\n {\n rootLinkOrigin,\n selectedNodeId: panel?.node.id ?? null,\n dotRadiusMemory: dotRadiusMemoryRef.current,\n nodeFillOf: (node) => getGenreNodeStyle(index, node.id).fill,\n },\n );\n });\n });\n\n coreHierarchyByRootId.forEach((branches, rootId) => {\n const rootLinkOrigin = getRadialPointOnCircle(\n rootAngleByRootId.get(rootId) ?? 0,\n coreRootCircleRadius,\n );\n\n const sectorGroup = originGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-core-sector\")\n .attr(\"data-gtv-root-id\", rootId);\n\n branches.forEach(({ hierarchy, angle, span }, branchIndex) => {\n const undergroundIds = new Set(\n hierarchy\n .descendants()\n .filter((d) => d.ancestors().some((a) => a.data.scene === \"underground\"))\n .map((d) => d.data.id),\n );\n const laidOut = computeCoreRadialLayout(\n d3,\n hierarchy,\n angle,\n span,\n coreRootCircleRadius,\n POP_TREE_DEPTH_RADIAL_SPACING,\n wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS,\n );\n const reparentForbiddenIds = reparentingNodeId\n ? (laidOut\n .descendants()\n .find((d) => d.data.id === reparentingNodeId)\n ?.descendants()\n .map((d) => d.data.id) ?? [])\n : [];\n\n const angleOf = (d: d3.HierarchyNode<GenreTreeNode>) => {\n const deg = (Math.atan2(d.x!, -d.y!) * 180) / Math.PI;\n return angle + ((((deg - angle + 540) % 360) + 360) % 360) - 180;\n };\n const subtreeRange = (d: d3.HierarchyNode<GenreTreeNode>) => {\n const angles = d.descendants().map(angleOf);\n return { min: Math.min(...angles), max: Math.max(...angles) };\n };\n laidOut\n .descendants()\n .filter((d) => d.data.scene === \"underground\" && !d.ancestors().slice(1).some((a) => a.data.scene === \"underground\"))\n .forEach((ug) => {\n const mine = subtreeRange(ug);\n const siblings = ug.parent?.children ?? [];\n const at = siblings.indexOf(ug);\n const prev = at > 0 ? siblings[at - 1] : undefined;\n const next = at >= 0 ? siblings[at + 1] : undefined;\n const startDeg = prev ? (subtreeRange(prev).max + mine.min) / 2 : angle - span / 2;\n const endDeg = next ? (mine.max + subtreeRange(next).min) / 2 : angle + span / 2;\n const arcPath = d3.arc()({\n innerRadius: coreRootCircleRadius,\n outerRadius: SCENE_SECTOR_OUTER_RADIUS,\n startAngle: (startDeg * Math.PI) / 180,\n endAngle: (endDeg * Math.PI) / 180,\n });\n sectorGroup\n .append(\"path\")\n .attr(\"class\", \"gtv-wheel-scene-sector\")\n .attr(\"d\", arcPath)\n .attr(\"fill\", shadeSurface(getGenreNodeColor(index, rootId), UNDERGROUND_DARKEN_RATIO))\n .attr(\"fill-opacity\", UNDERGROUND_SECTOR_FILL_OPACITY);\n });\n\n const branchGroup = sectorGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-core-branch\")\n .attr(\"data-gtv-branch-index\", branchIndex);\n\n renderPopSubtree(\n d3,\n branchGroup,\n laidOut,\n getGenreNodeColor(index, rootId),\n reparentingNodeId,\n reparentForbiddenIds,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId)\n void onReparent?.(reparentingNodeId, newParentId);\n },\n onNodeClick: (data, event, clickedElement) => {\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n onNodeDoubleClick: (data) => focusOnNodeRef.current(data.id),\n onNodeHover: (data) => onNodeHoverRef.current?.(data),\n additionalActions,\n playingNodeId,\n playState,\n },\n wheelItemCountRange,\n {\n isCoreSector: true,\n rootLinkOrigin,\n selectedNodeId: panel?.node.id ?? null,\n dotRadiusMemory: dotRadiusMemoryRef.current,\n nodeFillOf: (node) => {\n const fill = getGenreNodeStyle(index, node.id).fill;\n return undergroundIds.has(node.id) ? shadeSurface(fill, UNDERGROUND_DARKEN_RATIO) : fill;\n },\n },\n );\n });\n });\n\n if (centerSubtreeHierarchy) {\n const laidOutCenter = computeCenterRadialLayout(\n d3,\n centerSubtreeHierarchy,\n 0,\n POP_TREE_DEPTH_RADIAL_SPACING,\n );\n const reparentForbiddenIds = reparentingNodeId\n ? (laidOutCenter\n .descendants()\n .find((d) => d.data.id === reparentingNodeId)\n ?.descendants()\n .map((d) => d.data.id) ?? [])\n : [];\n\n const centerSectorGroup = originGroup\n .append(\"g\")\n .attr(\"class\", \"gtv-wheel-center-sector\");\n\n renderPopSubtree(\n d3,\n centerSectorGroup,\n laidOutCenter,\n centerNodeColor,\n reparentingNodeId,\n reparentForbiddenIds,\n {\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparentTargetSelect: (newParentId) => {\n if (reparentingNodeId)\n void onReparent?.(reparentingNodeId, newParentId);\n },\n onNodeClick: (data, event, clickedElement) => {\n centerOnElementRef.current(\n clickedElement,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(clickedElement, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfoRef.current(\n data,\n clickedElement,\n viewportRef.current,\n );\n onNodeClick?.(data, event);\n },\n onNodeDoubleClick: (data) => focusOnNodeRef.current(data.id),\n onNodeHover: (data) => onNodeHoverRef.current?.(data),\n additionalActions,\n playingNodeId,\n playState,\n },\n wheelItemCountRange,\n {\n skipRootNode: true,\n isMainstreamSector: isCenterMainstream,\n selectedNodeId: panel?.node.id ?? null,\n dotRadiusMemory: dotRadiusMemoryRef.current,\n nodeFillOf: (node) => getGenreNodeStyle(index, node.id).fill,\n },\n );\n }\n\n // Each branch draws its own links, so a later branch's links would paint over earlier ones' labels.\n const linksLayer = originGroup.insert(\"g\", \":first-child\").attr(\"class\", \"gtv-wheel-links\").node()!;\n originGroup.selectAll<SVGPathElement, unknown>(\"path.gtv-link\").each(function () {\n linksLayer.appendChild(this);\n });\n\n renderRootLabels(originGroup, [\n {\n id: centerNode.id,\n name: centerNode.name,\n x: 0,\n y: 0,\n radius: centerChipDiameter / 2,\n itemCount: centerNode.itemCount,\n alwaysShown: true,\n },\n ...groups.map((group) => {\n const itemCount = rootItemCountById.get(group.root.id)!;\n const { x, y } = getRadialPointOnCircle(rootAngleByRootId.get(group.root.id) ?? 0, coreRootCircleRadius);\n return {\n id: group.root.id,\n name: group.root.name,\n x,\n y,\n radius: calculateNodeDotRadius(itemCount, wheelItemCountRange),\n itemCount,\n };\n }),\n ]);\n hideCollidingLabels(d3, originGroup, wheelItemCountRange, zoomScaleRef.current);\n // A root's name usually spills past its chip, so the label itself forwards clicks to the chip.\n originGroup\n .selectAll<SVGGElement, RootLabelDatum>(\"g.gtv-root-label\")\n .select(\"text\")\n .style(\"pointer-events\", \"all\")\n .style(\"cursor\", \"pointer\")\n .on(\"click\", (_event, d) => {\n const chip =\n d.id === centerNode.id\n ? centerChipRef.current\n : viewportRef.current?.querySelector<HTMLButtonElement>(`#group-${CSS.escape(d.id)}`);\n chip?.click();\n })\n .on(\"dblclick\", (_event, d) => chipClickRef.current.doubleClick(() => focusOnNodeRef.current(d.id)));\n }, [\n centerNode,\n centerHasPopDisc,\n centerChipDiameter,\n groups,\n rootItemCountById,\n index,\n popHierarchyByRootId,\n coreHierarchyByRootId,\n coreRootCircleRadius,\n svgCanvasRadius,\n mainstreamCircleRadius,\n rootAngleByRootId,\n reparentingNodeId,\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n onReparent,\n onNodeClick,\n additionalActions,\n centerSubtreeHierarchy,\n centerNodeColor,\n isCenterMainstream,\n wheelItemCountRange,\n panel?.node.id,\n ]);\n\n // Re-places dot labels once a zoom gesture settles, so the set of named nodes follows the zoom\n // like place names on a map — kept out of the render effect above, which must not re-mount on zoom.\n useEffect(() => {\n const timeout = setTimeout(() => {\n if (popSvgRef.current) hideCollidingLabels(d3, d3.select(popSvgRef.current), wheelItemCountRange, zoomScale);\n }, LABEL_RELAYOUT_DEBOUNCE_MS);\n return () => clearTimeout(timeout);\n }, [zoomScale, wheelItemCountRange]);\n\n // Google Maps-style region label: once pan/zoom settles with the viewport center inside a ring\n // root's sector whose own chip is off-screen, names that root in an empty spot. Kept in place\n // until it leaves the viewport, its root changes, or zoom drifts its on-screen size out of range.\n const { panX, panY } = panZoom;\n useEffect(() => {\n const timeout = setTimeout(() => {\n const viewport = viewportRef.current;\n const wheel = wheelCircleRef.current;\n if (!viewport || !wheel) return;\n const vp = viewport.getBoundingClientRect();\n const wheelRect = wheel.getBoundingClientRect();\n const wheelX = wheelRect.left + wheelRect.width / 2;\n const wheelY = wheelRect.top + wheelRect.height / 2;\n const sectors = groups.map((group) => ({\n id: group.root.id,\n center: continuousAngleByRootId.get(group.root.id) ?? 0,\n span: sectorSpanByRootId.get(group.root.id) ?? 360,\n }));\n const rootIdAt = (x: number, y: number) =>\n Math.hypot(x - wheelX, y - wheelY) <= mainstreamCircleRadius * zoomScale\n ? null\n : findSectorAtAngle((Math.atan2(x - wheelX, wheelY - y) * 180) / Math.PI, sectors);\n const isOnScreen = (r: DOMRect) =>\n r.right > vp.left && r.left < vp.right && r.bottom > vp.top && r.top < vp.bottom;\n\n const rootId = rootIdAt(vp.left + vp.width / 2, vp.top + vp.height / 2);\n const chip = rootId && viewport.querySelector(`#group-${CSS.escape(rootId)}`);\n if (!rootId || !chip || isOnScreen(chip.getBoundingClientRect())) {\n setRegionLabel(null);\n return;\n }\n\n const shown = regionLabelRef.current?.getBoundingClientRect();\n const shownPx = (regionLabel?.fontSize ?? 0) * zoomScale;\n if (\n regionLabel?.rootId === rootId &&\n shown &&\n shown.left >= vp.left &&\n shown.right <= vp.right &&\n shown.top >= vp.top &&\n shown.bottom <= vp.bottom &&\n shownPx >= REGION_LABEL_MIN_PX &&\n shownPx <= REGION_LABEL_MAX_PX\n )\n return;\n\n const name = index.byId.get(rootId)?.name ?? \"\";\n const obstacles = Array.from(\n viewport.querySelectorAll(\n \".gtv-node-dot, .gtv-node-label:not(.gtv-node-label--hidden), .gtv-root-label, .gtv-wheel-chip, .gtv-wheel-floating-controls, .gtv-info-panel\",\n ),\n )\n .map((element) => element.getBoundingClientRect())\n .filter((r) => r.width > 0 && r.height > 0)\n .map((r) => ({ x0: r.left, y0: r.top, x1: r.right, y1: r.bottom }));\n const inset = REGION_LABEL_TARGET_PX / 2;\n const spot = pickEmptyLabelSpot(\n { x0: vp.left + inset, y0: vp.top + inset, x1: vp.right - inset, y1: vp.bottom - inset },\n name.length * REGION_LABEL_TARGET_PX * LABEL_CHAR_WIDTH_RATIO,\n REGION_LABEL_TARGET_PX,\n obstacles,\n (x, y) => rootIdAt(x, y) === rootId,\n );\n setRegionLabel(\n spot && {\n rootId,\n x: (spot.x - vp.left - panX) / zoomScale,\n y: (spot.y - vp.top - panY) / zoomScale,\n fontSize: REGION_LABEL_TARGET_PX / zoomScale,\n },\n );\n }, LABEL_RELAYOUT_DEBOUNCE_MS);\n return () => clearTimeout(timeout);\n }, [\n panX,\n panY,\n zoomScale,\n groups,\n continuousAngleByRootId,\n sectorSpanByRootId,\n mainstreamCircleRadius,\n index,\n regionLabel,\n ]);\n\n // Starts the view fit to the wheel + rendered pop sectors instead of at scale 1 / pan (0, 0) —\n // guarded so it only fires once popSvgRef has actually rendered content, and never again\n // afterward so it doesn't fight the user's own pan/zoom on later selections/expansions.\n const hasInitialFitRef = useRef(false);\n useEffect(() => {\n hasInitialFitRef.current = false;\n }, [focusNodeId]);\n useEffect(() => {\n if (hasInitialFitRef.current) return;\n const elements = [\n wheelCircleRef.current,\n ...queryTreeContentElements(popSvgRef.current),\n ];\n if (!elements.some(Boolean)) return;\n hasInitialFitRef.current = true;\n panZoom.fitToFrame(elements, { silent: true });\n });\n\n const handleChipClick = (rootId: string) => {\n setTopRootId(rootId);\n if (allowWheelRotation) setRotationTopRootId(rootId);\n };\n\n // Mirrors the onNodeClick/onSelectNode handling below: once the node's sector is mounted, center the viewport on the\n // externally-selected node and open its info panel exactly as a direct click would.\n useEffect(() => {\n if (!selectedNodeId || openPanel?.node.id === selectedNodeId) return;\n const targetNode = index.byId.get(selectedNodeId);\n if (\n targetNode &&\n onFocusChange &&\n !centerSubtreeNodeIds.has(selectedNodeId) &&\n !ringRootById.has(selectedNodeId)\n ) {\n // Outside the current zoom (e.g. a search result): refocus on its top-level root, whose frame\n // holds its whole subtree (the whole tree for Mainstream Pop's lineage or a root itself);\n // this effect re-runs once the new frame is drawn.\n const [root] = index.ancestorsOf(selectedNodeId);\n onFocusChange(root && root.name !== CENTER_NODE_NAME && isZoomableNode(nodes, root.id) ? root.id : null, \"search\");\n return;\n }\n const element = viewportRef.current?.querySelector(\n `#group-${CSS.escape(selectedNodeId)}`,\n );\n if (!targetNode || !element) return;\n panZoom.centerOnElement(\n element,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),\n );\n showNodeInfo(targetNode, element, viewportRef.current, { notify: false });\n // eslint-disable-next-line react-hooks/exhaustive-deps -- panZoom re-created on pan/zoom; guarded by the openPanel?.node.id check above\n }, [selectedNodeId, index, openPanel, showNodeInfo, centerSubtreeNodeIds, ringRootById]);\n\n // One divider per boundary between two angularly-adjacent ring roots — see WheelRadialCore's own\n // copy of this computation for why each root's own continuous angle plus half its\n // weight-proportional width (sectorSpanByRootId) lands exactly on the boundary with its next\n // neighbor.\n const dividerAngles = useMemo(() => {\n if (groups.length <= 1) return [];\n return groups.map((group) => {\n const angle = continuousAngleByRootId.get(group.root.id) ?? 0;\n const width = sectorSpanByRootId.get(group.root.id) ?? 0;\n return angle + width / 2;\n });\n }, [groups, continuousAngleByRootId, sectorSpanByRootId]);\n\n // One tinted sector fan per root, bounded by its own weight-proportional width — see\n // WheelRadialCore's own copy of this computation, and computeSectorWidths's doc comment.\n const sectorFills = useMemo(() => {\n if (groups.length <= 1) return [];\n return groups.map((group) => {\n const angle = continuousAngleByRootId.get(group.root.id) ?? 0;\n const width = sectorSpanByRootId.get(group.root.id) ?? 0;\n return {\n rootId: group.root.id,\n start: angle - width / 2,\n clipPath: buildSectorClipPathPolygon(width),\n color: hexToRgba(\n getGenreNodeColor(index, group.root.id),\n ROOT_SECTOR_FILL_OPACITY,\n ),\n };\n });\n }, [groups, continuousAngleByRootId, sectorSpanByRootId, index]);\n\n return (\n <div\n ref={viewportRef}\n aria-busy={busy}\n className={[\n \"gtv-wheel-container\",\n \"gtv-wheel-container--radial\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n \"--gtv-wheel-radius\": `${coreRootCircleRadius}px`,\n \"--gtv-wheel-svg-radius\": `${svgCanvasRadius}px`,\n \"--gtv-wheel-rotation-transition-ms\": `${WHEEL_ROTATION_TRANSITION_MS}ms`,\n \"--gtv-wheel-rotation-easing\": WHEEL_ROTATION_EASING,\n touchAction: panZoom.touchAction,\n } as React.CSSProperties\n }\n onPointerDown={panZoom.handlePointerDown}\n >\n <div\n ref={stageRef}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n transform: panZoom.transform,\n transformOrigin: \"0 0\",\n }}\n >\n <div className=\"gtv-wheel-stage\">\n <div className=\"gtv-wheel-circle\" ref={wheelCircleRef} />\n\n {isPopExpanded && (\n <div\n className=\"gtv-wheel-middle-circle\"\n style={\n {\n \"--gtv-wheel-middle-radius\": `${middleCircleFloor}px`,\n } as React.CSSProperties\n }\n />\n )}\n\n {/* Sector wash + inner tint sit in their own .gtv-wheel layer, rendered before the node\n svg below, so they paint under the developed nodes instead of dulling them — see the\n second .gtv-wheel layer (after the svg) for the dividers/chips, which stay above the\n nodes. */}\n <div className=\"gtv-wheel\">\n {sectorFills.map((sector) => (\n <div\n key={`sector-${sector.rootId}`}\n className=\"gtv-wheel-sector\"\n style={\n {\n \"--gtv-sector-angle\": `${sector.start}deg`,\n \"--gtv-sector-color\": sector.color,\n clipPath: sector.clipPath,\n } as React.CSSProperties\n }\n />\n ))}\n\n <div className=\"gtv-wheel-inner-tint\" />\n\n {isPopExpanded && (\n <div\n className=\"gtv-wheel-middle-tint\"\n style={\n {\n \"--gtv-wheel-middle-radius\": `${middleCircleFloor}px`,\n } as React.CSSProperties\n }\n />\n )}\n </div>\n\n <svg\n ref={popSvgRef}\n className=\"gtv-wheel-pop-layer\"\n width={svgCanvasRadius * 2}\n height={svgCanvasRadius * 2}\n />\n\n <div className=\"gtv-wheel-center-node\">\n <div className=\"gtv-wheel-chip-anchor\">\n <button\n ref={centerChipRef}\n type=\"button\"\n className={[\n \"gtv-wheel-chip\",\n centerHasPopDisc && \"gtv-wheel-chip--center\",\n \"gtv-wheel-chip--circle\",\n ]\n .filter(Boolean)\n .join(\" \")}\n style={\n {\n width: centerChipDiameter,\n \"--gtv-wheel-chip-base-height\": `${centerChipDiameter}px`,\n \"--gtv-chip-color\": centerNodeColor,\n ...(panel?.node.id === centerNode.id && {\n borderColor: ACCENT_COLOR,\n borderWidth: calculateSelectedNodeBorderWidth(centerChipDiameter / 2),\n }),\n } as React.CSSProperties\n }\n onClick={(event) => {\n const chip = event.currentTarget;\n const nativeEvent = event.nativeEvent;\n chipClickRef.current.schedule(() => {\n panZoom.centerOnElement(\n chip,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(chip, viewportRef.current, INFO_PANEL_WIDTH),\n );\n if (!reparentingNodeId) {\n showNodeInfo(centerNode, chip, viewportRef.current);\n onNodeClick?.(centerNode, nativeEvent);\n }\n }, () => focusOnNode(centerNode.id), centerNode.id);\n }}\n onDoubleClick={() => chipClickRef.current.doubleClick(() => focusOnNode(centerNode.id))}\n aria-label={centerNode.name}\n />\n </div>\n </div>\n\n <div className=\"gtv-wheel\">\n {dividerAngles.map((angle, index) => (\n <div\n key={`divider-${groups[index].root.id}`}\n className=\"gtv-wheel-divider\"\n style={\n {\n \"--gtv-divider-angle\": `${angle}deg`,\n } as React.CSSProperties\n }\n />\n ))}\n\n {groups.map((group, groupIndex) => {\n const slot = layout[groupIndex];\n const root = index.byId.get(group.root.id)!;\n const angle =\n continuousAngleByRootId.get(group.root.id) ?? slot?.angle ?? 0;\n const chipColor = getGenreNodeStyle(index, group.root.id).fill;\n const rootItemCount = rootItemCountById.get(\n group.root.id,\n )!;\n const diameter = 2 * calculateNodeDotRadius(rootItemCount, wheelItemCountRange);\n return (\n <div\n key={group.root.id}\n className=\"gtv-wheel-slot\"\n style={\n { \"--gtv-chip-angle\": `${angle}deg` } as React.CSSProperties\n }\n >\n <div className=\"gtv-wheel-chip-anchor\">\n <button\n type=\"button\"\n id={`group-${group.root.id}`}\n className=\"gtv-wheel-chip gtv-wheel-chip--selected gtv-wheel-chip--circle\"\n style={\n {\n width: diameter,\n \"--gtv-wheel-chip-base-height\": `${diameter}px`,\n \"--gtv-chip-color\": chipColor,\n ...(panel?.node.id === group.root.id && {\n borderColor: ACCENT_COLOR,\n borderWidth: calculateSelectedNodeBorderWidth(diameter / 2),\n }),\n } as React.CSSProperties\n }\n aria-label={group.root.name}\n onPointerEnter={() => onNodeHover?.(root)}\n onClick={(event) => {\n const chip = event.currentTarget;\n const nativeEvent = event.nativeEvent;\n chipClickRef.current.schedule(() => {\n panZoom.centerOnElement(\n chip,\n ZOOM_FOCUS_SCALE,\n resolveInfoPanelObscuredArea(chip, viewportRef.current, INFO_PANEL_WIDTH),\n );\n handleChipClick(group.root.id);\n if (!reparentingNodeId) {\n showNodeInfo(root, chip, viewportRef.current);\n onNodeClick?.(root, nativeEvent);\n }\n }, () => focusOnNode(group.root.id), group.root.id);\n }}\n onDoubleClick={() => chipClickRef.current.doubleClick(() => focusOnNode(group.root.id))}\n />\n </div>\n </div>\n );\n })}\n </div>\n </div>\n {regionLabel && index.byId.has(regionLabel.rootId) && (\n <div\n ref={regionLabelRef}\n className=\"gtv-wheel-region-label\"\n aria-hidden=\"true\"\n style={\n {\n \"--gtv-region-label-x\": `${regionLabel.x}px`,\n \"--gtv-region-label-y\": `${regionLabel.y}px`,\n \"--gtv-region-label-size\": `${regionLabel.fontSize}px`,\n \"--gtv-region-label-color\": getGenreNodeColor(index, regionLabel.rootId),\n } as React.CSSProperties\n }\n >\n {index.byId.get(regionLabel.rootId)!.name}\n </div>\n )}\n </div>\n\n {panel && (\n <InfoPanel\n node={panel.node}\n {...getNodeVisualStyle(panel.node)}\n parentNode={(() => {\n const parent = index.byId.get(index.byId.get(panel.node.id)?.parentId ?? \"\");\n return parent\n ? { node: parent, ...getNodeVisualStyle(parent) }\n : null;\n })()}\n childNodes={(index.childrenByParentId.get(panel.node.id) ?? []).map(\n (n) => ({ node: n, ...getNodeVisualStyle(n) }),\n )}\n ancestorNodes={index.ancestorsOf(panel.node.id).map(\n (n): InfoPanelChild => ({ node: n, ...getNodeVisualStyle(n) }),\n )}\n side={panel.side}\n onClose={closeNodeInfo}\n onSelectNode={selectNodeFromPanel}\n onPrevious={\n typeof previousSelectedId === \"string\"\n ? () => {\n markPreviousRequested(previousSelectedId);\n selectNodeFromPanel(previousSelectedId);\n }\n : undefined\n }\n previousLabel={typeof previousSelectedId === \"string\" ? index.byId.get(previousSelectedId)?.name : undefined}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause,\n onAddChild,\n onRenameRequest,\n onDeleteRequest,\n onReparentRequest,\n additionalActions,\n }\n : undefined\n }\n renderInfoPanelBody={renderInfoPanelBody}\n onZoomIn={\n onFocusChange && panel.node.id !== focusNodeId && isZoomableNode(nodes, panel.node.id)\n ? () => onFocusChange(panel.node.id, \"zoom-in\")\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n\n {onFocusChange && focusNodeId !== null && (\n <WheelBreadcrumb\n key={focusNodeId}\n ancestors={index.ancestorsOf(focusNodeId).filter((ancestor) => isZoomableNode(nodes, ancestor.id))}\n currentName={index.byId.get(focusNodeId)?.name}\n onHome={() => {\n closeNodeInfo();\n onFocusChange(null, \"root\");\n }}\n onAncestor={(id) => {\n closeNodeInfo();\n onFocusChange(id, \"back\");\n }}\n />\n )}\n\n <FloatingControls\n panZoom={panZoom}\n orientation={controlsOrientation}\n zoomControls={showZoomControls}\n leading={floatingControlsLeading}\n busy={busy}\n onFit={() => panZoom.fitToFrame([\n wheelCircleRef.current,\n ...queryTreeContentElements(popSvgRef.current),\n ])}\n />\n </div>\n );\n}\n","import { useState } from \"react\";\n\ninterface FocusHistoryState {\n seen: string | null;\n stack: (string | null)[];\n pending: string | null | undefined;\n}\n\n/** Remembers the focus the wheel actually displayed, whatever changed it (wheel buttons, search,\n * rejection, or the consumer). `previous` is the latest remembered view that is still focusable and\n * differs from the current one; `undefined` means there is nothing to go back to. */\nexport function useFocusHistory(focusNodeId: string | null, isFocusable: (id: string) => boolean) {\n const [state, setState] = useState<FocusHistoryState>({ seen: focusNodeId, stack: [], pending: undefined });\n\n if (state.seen !== focusNodeId) {\n // ponytail: unbounded, in-memory only; cap it if sessions ever get long enough to matter.\n const popTo = state.pending === focusNodeId ? state.stack.lastIndexOf(focusNodeId) : -1;\n setState({\n seen: focusNodeId,\n stack: popTo >= 0 ? state.stack.slice(0, popTo) : [...state.stack, state.seen],\n pending: undefined,\n });\n }\n\n let previous: string | null | undefined;\n for (let i = state.stack.length - 1; i >= 0; i--) {\n const entry = state.stack[i];\n if (entry !== focusNodeId && (entry === null || isFocusable(entry))) {\n previous = entry;\n break;\n }\n }\n\n const markPreviousRequested = (id: string | null) => setState((current) => ({ ...current, pending: id }));\n\n return { previous, markPreviousRequested };\n}\n","import { Fragment, useState } from \"react\";\nimport { MdHome } from \"react-icons/md\";\n\nimport type { GenreTreeNode } from \"./types\";\n\nconst MAX_VISIBLE_ANCESTORS = 3;\nconst ANCESTORS_KEPT_WHEN_COLLAPSED = 2;\n\ninterface WheelBreadcrumbProps {\n /** Zoomable ancestors of the focused view, root first. */\n ancestors: readonly GenreTreeNode[];\n currentName: string | undefined;\n onHome: () => void;\n onAncestor: (id: string) => void;\n}\n\n/** The focused wheel's trail: Home, its zoomable ancestor views, then the current view. A long trail\n * collapses its middle into a \"…\" button that expands it inline. */\nexport function WheelBreadcrumb({ ancestors, currentName, onHome, onAncestor }: WheelBreadcrumbProps) {\n const [expanded, setExpanded] = useState(false);\n const collapsed = !expanded && ancestors.length > MAX_VISIBLE_ANCESTORS;\n const visibleAncestors = collapsed ? ancestors.slice(-ANCESTORS_KEPT_WHEN_COLLAPSED) : ancestors;\n const separator = <span aria-hidden=\"true\">›</span>;\n\n return (\n <nav className=\"gtv-wheel-breadcrumb\" aria-label=\"Breadcrumb\">\n <button\n type=\"button\"\n className=\"gtv-wheel-breadcrumb-crumb gtv-wheel-breadcrumb-home\"\n aria-label=\"All genres\"\n title=\"All genres\"\n onClick={onHome}\n >\n <MdHome className=\"gtv-icon\" size={14} />\n </button>\n {separator}\n {collapsed && (\n <>\n <button\n type=\"button\"\n className=\"gtv-wheel-breadcrumb-crumb\"\n aria-label=\"Show full path\"\n aria-expanded={false}\n onClick={() => setExpanded(true)}\n >\n …\n </button>\n {separator}\n </>\n )}\n {visibleAncestors.map((ancestor) => (\n <Fragment key={ancestor.id}>\n <button type=\"button\" className=\"gtv-wheel-breadcrumb-crumb\" onClick={() => onAncestor(ancestor.id)}>\n {ancestor.name}\n </button>\n {separator}\n </Fragment>\n ))}\n <span className=\"gtv-wheel-breadcrumb-current\" aria-current=\"page\">\n {currentName}\n </span>\n </nav>\n );\n}\n","import { CENTER_NODE_NAME } from \"./constants\";\nimport { GenreTreeNode } from \"./types\";\n\nexport interface PopCoreFrame {\n /** The node rendered at the wheel's center. */\n center: GenreTreeNode;\n /** True when the center renders as the white pop disc with its own subtree inside; false when\n * it's shown as a label only (its children are the ring roots instead). */\n centerHasPopDisc: boolean;\n /** The center plus, when it has a pop disc, its whole subtree. */\n centerNodes: GenreTreeNode[];\n /** Every node rendered on the ring, each ring root's `parentId` rewritten to null (as is the\n * center's, so each subtree stratifies on its own). */\n ringNodes: GenreTreeNode[];\n}\n\nfunction collectSubtree(root: GenreTreeNode, childrenByParentId: Map<string, GenreTreeNode[]>) {\n const subtree: GenreTreeNode[] = [];\n const stack = [root];\n while (stack.length > 0) {\n const current = stack.pop()!;\n subtree.push(current);\n stack.push(...(childrenByParentId.get(current.id) ?? []));\n }\n return subtree;\n}\n\n/**\n * Whether `id` can be the pop-core wheel's focus: a known node with children and at most one pop\n * child (`resolvePopCoreFrame` can't center more than one pop disc).\n */\nexport function isZoomableNode(nodes: readonly GenreTreeNode[], id: string): boolean {\n if (!nodes.some((node) => node.id === id)) return false;\n const children = nodes.filter((node) => node.parentId === id);\n return children.length > 0 && children.filter((child) => child.side === \"pop\").length <= 1;\n}\n\n/**\n * Picks what the pop-core wheel renders. Without a focus, the \"Mainstream Pop\" root is the center\n * and every other root is a ring root. With a focus, the focus node's single pop-side child (if\n * any) becomes the center pop disc, otherwise the focus node itself is the center (label only);\n * every other direct child of the focus node becomes a ring root.\n */\nexport function resolvePopCoreFrame(nodes: GenreTreeNode[], focusNodeId: string | null): PopCoreFrame {\n const childrenByParentId = new Map<string, GenreTreeNode[]>();\n nodes.forEach((node) => {\n if (node.parentId === null) return;\n const siblings = childrenByParentId.get(node.parentId);\n if (siblings) siblings.push(node);\n else childrenByParentId.set(node.parentId, [node]);\n });\n\n if (focusNodeId === null) {\n const center = nodes.find((node) => node.parentId === null && node.name === CENTER_NODE_NAME);\n if (!center) {\n throw new Error(`GenreTreeWheelRadialPopCore requires a root node named \"${CENTER_NODE_NAME}\"`);\n }\n const centerNodes = collectSubtree(center, childrenByParentId);\n const centerIds = new Set(centerNodes.map((node) => node.id));\n return {\n center,\n centerHasPopDisc: true,\n centerNodes,\n ringNodes: nodes.filter((node) => !centerIds.has(node.id)),\n };\n }\n\n const focus = nodes.find((node) => node.id === focusNodeId);\n if (!focus) throw new Error(`Unknown pop-core focus node \"${focusNodeId}\"`);\n const children = childrenByParentId.get(focus.id) ?? [];\n const popChildren = children.filter((child) => child.side === \"pop\");\n if (popChildren.length > 1) {\n throw new Error(`Pop-core focus node \"${focus.id}\" has more than one pop child`);\n }\n const asRoot = (root: GenreTreeNode) =>\n collectSubtree(root, childrenByParentId).map((node) => (node === root ? { ...node, parentId: null } : node));\n const [popChild] = popChildren;\n const ringNodes = children.filter((child) => child !== popChild).flatMap(asRoot);\n if (!popChild) {\n const center = { ...focus, parentId: null };\n return { center, centerHasPopDisc: false, centerNodes: [center], ringNodes };\n }\n const centerNodes = asRoot(popChild);\n return { center: centerNodes[0], centerHasPopDisc: true, centerNodes, ringNodes };\n}\n","\"use client\";\n\nimport { WheelRadialPopCoreCore, WheelRadialPopCoreProps } from \"./GenreTreeWheelRadialPopCoreBase\";\n\nexport type GenreTreeWheelRadialPopCoreProps = WheelRadialPopCoreProps;\n\n/**\n * `GenreTreeWheelRadial`, but each developed root's outward subtree is only its core (non-pop)\n * branch — if that root also has a `side: \"pop\"` child, that pop subtree renders as a full\n * interactive tree fanned out inside the wheel's own circle instead. The wheel's pivot point\n * renders `nodes`' root named \"Pop\" (which must exist and have no children) as a full interactive\n * chip, styled the same as the ring's own root chips, instead of a plain label, and that root is\n * excluded from the ring's own chips.\n */\nexport function GenreTreeWheelRadialPopCore(props: GenreTreeWheelRadialPopCoreProps) {\n return <WheelRadialPopCoreCore {...props} />;\n}\n","\"use client\";\n\nimport {\n memo,\n MouseEvent as ReactMouseEvent,\n useLayoutEffect,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\n\nimport { InfoPanel, InfoPanelChild } from \"./InfoPanel\";\nimport { OverlayPanel } from \"./OverlayPanel\";\nimport { PopCoreSplit, splitRootGroupBySide } from \"./pop-core-split\";\nimport { groupNodesByRoot } from \"./root-grouping\";\nimport { TreeIndex } from \"./tree-index\";\nimport { GenreTreeAction, GenreTreeNode, GenreTreeProps } from \"./types\";\nimport { useTreeIndex } from \"./use-tree-index\";\nimport {\n ACCENT_TEXT_COLOR,\n CENTER_NODE_NAME,\n getGenreTreeColor,\n POP_SECTOR_TINT_RATIO,\n TEXT_COLOR,\n TEXT_MUTED_COLOR,\n tintSurface,\n} from \"./constants\";\n\nexport type GenreTreeOutlineProps = Omit<\n GenreTreeProps,\n | \"rootColor\"\n | \"orientation\"\n | \"hideRoot\"\n | \"interactive\"\n | \"depthSpacingScale\"\n | \"wheelZoom\"\n | \"controlsOrientation\"\n | \"showZoomControls\"\n | \"floatingControlsLeading\"\n | \"busy\"\n>;\n\ninterface OutlineState {\n selectedId: string | null;\n /** Node ids, plus `sectionKey`s, whose `<details>` is expanded. */\n openIds: ReadonlySet<string>;\n reparentingNodeId: string | null;\n reparentForbiddenIds: ReadonlySet<string>;\n}\n\ninterface OutlineStore {\n get: () => OutlineState;\n set: (patch: Partial<OutlineState>) => void;\n subscribe: (listener: () => void) => () => void;\n}\n\nfunction createOutlineStore(initial: OutlineState): OutlineStore {\n let state = initial;\n const listeners = new Set<() => void>();\n return {\n get: () => state,\n set: (patch) => {\n state = { ...state, ...patch };\n listeners.forEach((listener) => listener());\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n\n// Selectors return primitives, so a row only re-renders when its own slice changes.\nfunction useOutlineState<T>(store: OutlineStore, selector: (state: OutlineState) => T): T {\n return useSyncExternalStore(store.subscribe, () => selector(store.get()));\n}\n\ntype SectionKind = \"core\" | \"pop\";\n// NUL prefix keeps section keys from ever colliding with a node id.\nconst sectionKey = (kind: SectionKind, rootId: string) => `\\u0000${kind}:${rootId}`;\n\ninterface OutlineModel {\n index: TreeIndex;\n centerRoot: GenreTreeNode;\n ringRoots: GenreTreeNode[];\n detachedRoots: GenreTreeNode[];\n ringSplitById: ReadonlyMap<string, PopCoreSplit>;\n styleOf: (node: GenreTreeNode) => { fill: string; textColor: string };\n}\n\nfunction buildOutlineModel(nodes: readonly GenreTreeNode[], index: TreeIndex): OutlineModel {\n const groups = groupNodesByRoot([...nodes]);\n const centerGroup = groups.find((group) => group.root.name === CENTER_NODE_NAME);\n if (!centerGroup) {\n throw new Error(`GenreTreeOutline requires a root node named \"${CENTER_NODE_NAME}\"`);\n }\n const detachedGroups = groups.filter((group) => group.root.detached);\n const detachedRootIds = new Set(detachedGroups.map((group) => group.root.id));\n const ringGroups = groups.filter((group) => group !== centerGroup && !group.root.detached);\n const ringSplitById = new Map(ringGroups.map((group) => [group.root.id, splitRootGroupBySide(group)]));\n const popNodeIds = new Set([...ringSplitById.values()].flatMap((split) => split.popNodes.map((node) => node.id)));\n\n // Same fill/text rules as GenreTreeWheelRadialPopCore's own getNodeVisualStyle: center subtree\n // tinted with dark text, a ring root's core side solid, its pop side tinted.\n const styleOf = (node: GenreTreeNode) => {\n const rootId = index.rootIdById.get(node.id) ?? node.id;\n if (rootId === centerGroup.root.id) {\n return { fill: tintSurface(\"#ffffff\", POP_SECTOR_TINT_RATIO), textColor: TEXT_COLOR };\n }\n if (detachedRootIds.has(rootId)) return { fill: TEXT_MUTED_COLOR, textColor: TEXT_COLOR };\n const rootColor = getGenreTreeColor(index.byId.get(rootId)?.wikidataId);\n return {\n fill: popNodeIds.has(node.id) ? tintSurface(rootColor, POP_SECTOR_TINT_RATIO) : rootColor,\n textColor: ACCENT_TEXT_COLOR,\n };\n };\n\n return {\n index,\n centerRoot: centerGroup.root,\n ringRoots: ringGroups.map((group) => group.root),\n detachedRoots: detachedGroups.map((group) => group.root),\n ringSplitById,\n styleOf,\n };\n}\n\n/** Identity-stable for the component's lifetime; every handler reads the latest props. */\ninterface OutlineCallbacks {\n onNameClick: (event: ReactMouseEvent<HTMLButtonElement>, node: GenreTreeNode) => void;\n onHover: (node: GenreTreeNode) => void;\n toggleOpen: (key: string) => void;\n onPlayPause: (nodeId: string) => void;\n onAddChild: (parentId: string) => void;\n onRenameRequest: (node: GenreTreeNode) => void;\n onDeleteRequest: (node: GenreTreeNode) => void;\n onReparentRequest: (node: GenreTreeNode) => void;\n additionalActions: (node: GenreTreeNode) => GenreTreeAction[];\n}\n\ninterface OutlineItemProps {\n model: OutlineModel;\n store: OutlineStore;\n callbacks: OutlineCallbacks;\n}\n\nfunction handleSummaryClick(event: ReactMouseEvent, toggle: () => void) {\n // The name button and toolbar preventDefault their own clicks so they never toggle the section.\n if (event.defaultPrevented) return;\n event.preventDefault();\n toggle();\n}\n\n// Declared unwrapped and memoized separately: a named `memo(function OutlineNode …)` would bind\n// the recursive `<OutlineNode>` below to the unmemoized inner function.\nfunction OutlineNodeView({\n nodeId,\n isRoot,\n model,\n store,\n callbacks,\n}: OutlineItemProps & { nodeId: string; isRoot: boolean }) {\n const node = model.index.byId.get(nodeId)!;\n const isSelected = useOutlineState(store, (s) => s.selectedId === nodeId);\n const isOpen = useOutlineState(store, (s) => s.openIds.has(nodeId));\n const reparentStatus = useOutlineState(store, (s) =>\n s.reparentingNodeId === null ? null : s.reparentForbiddenIds.has(nodeId) ? \"forbidden\" : \"target\",\n );\n\n const { itemCount } = node;\n const children = model.index.childrenByParentId.get(nodeId) ?? [];\n const split = isRoot ? model.ringSplitById.get(nodeId) : undefined;\n\n const row = (\n <span className={[\"gtv-outline-row\", isSelected && \"gtv-outline-row--selected\"].filter(Boolean).join(\" \")}>\n <span className=\"gtv-outline-dot\" style={{ background: model.styleOf(node).fill }} />\n <button\n type=\"button\"\n className={[\n \"gtv-outline-name\",\n isRoot && \"gtv-outline-name--root\",\n node.umbrella && \"gtv-outline-name--umbrella\",\n reparentStatus === \"target\" && \"gtv-outline-name--reparent-target\",\n ]\n .filter(Boolean)\n .join(\" \")}\n disabled={reparentStatus === \"forbidden\"}\n onClick={(event) => callbacks.onNameClick(event, node)}\n onPointerEnter={() => callbacks.onHover(node)}\n onFocus={() => callbacks.onHover(node)}\n >\n {node.name}\n </button>\n <span className=\"gtv-outline-count\">{itemCount}</span>\n </span>\n );\n\n const itemProps = { model, store, callbacks };\n return (\n <li className=\"gtv-outline-item\" data-gtv-node-id={nodeId}>\n {children.length > 0 ? (\n <details open={isOpen}>\n <summary onClick={(event) => handleSummaryClick(event, () => callbacks.toggleOpen(nodeId))}>{row}</summary>\n {isOpen && (\n <ul className=\"gtv-outline-list\">\n {split ? (\n <>\n <OutlineSection rootId={nodeId} kind=\"core\" branches={split.coreBranches} {...itemProps} />\n <OutlineSection rootId={nodeId} kind=\"pop\" branches={split.popBranches} {...itemProps} />\n </>\n ) : (\n children.map((child) => <OutlineNode key={child.id} nodeId={child.id} isRoot={false} {...itemProps} />)\n )}\n </ul>\n )}\n </details>\n ) : (\n <div className=\"gtv-outline-leaf\">{row}</div>\n )}\n </li>\n );\n}\nconst OutlineNode = memo(OutlineNodeView);\n\nconst OutlineSection = memo(function OutlineSection({\n rootId,\n kind,\n branches,\n model,\n store,\n callbacks,\n}: OutlineItemProps & { rootId: string; kind: SectionKind; branches: PopCoreSplit[\"coreBranches\"] }) {\n const key = sectionKey(kind, rootId);\n const isOpen = useOutlineState(store, (s) => s.openIds.has(key));\n if (branches.length === 0) return null;\n return (\n <li className=\"gtv-outline-item\">\n <details open={isOpen}>\n <summary onClick={(event) => handleSummaryClick(event, () => callbacks.toggleOpen(key))}>\n <span className=\"gtv-outline-section-label\">{kind === \"core\" ? \"Core\" : \"Pop\"}</span>\n </summary>\n {isOpen && (\n <ul className=\"gtv-outline-list\">\n {branches.map(({ child }) => (\n <OutlineNode key={child.id} nodeId={child.id} isRoot={false} model={model} store={store} callbacks={callbacks} />\n ))}\n </ul>\n )}\n </details>\n </li>\n );\n});\n\n/**\n * Text counterpart of `GenreTreeWheelRadialPopCore`: the same forest as nested collapsible lists\n * (`<details>`), \"Mainstream Pop\" first, then every other root with its direct children split\n * into \"Core\" and \"Pop\" sections. Same toolbar, reparent and info-panel behavior as the graphical\n * renderers.\n *\n * Selection and open sections live in a per-instance external store that each memoized row\n * subscribes to by its own id, so selecting a node re-renders only the two affected rows and the\n * panel, and a collapsed section's rows aren't rendered at all.\n */\nexport function GenreTreeOutline(props: GenreTreeOutlineProps) {\n const {\n nodes,\n className,\n playingNodeId = null,\n playState,\n reparentingNodeId = null,\n showToolbar = true,\n selectedNodeId,\n renderInfoPanelBody,\n hideInfoPanelClose = false,\n hideInfoPanelTitle = false,\n itemCountLabel,\n readOnly,\n renderDangerZone,\n expanded,\n onExpandedChange,\n collapsed,\n hideExpandButton,\n overlayPanel,\n } = props;\n const scrollRef = useRef<HTMLDivElement>(null);\n const index = useTreeIndex(nodes);\n const model = useMemo(() => buildOutlineModel(nodes, index), [nodes, index]);\n\n const reparentForbiddenIds = useMemo(() => {\n const ids = new Set<string>();\n const stack = reparentingNodeId ? [reparentingNodeId] : [];\n while (stack.length > 0) {\n const id = stack.pop()!;\n ids.add(id);\n stack.push(...(index.childrenByParentId.get(id) ?? []).map((child) => child.id));\n }\n return ids;\n }, [reparentingNodeId, index]);\n\n const [store] = useState(() =>\n createOutlineStore({\n selectedId: null,\n openIds: new Set(),\n reparentingNodeId,\n reparentForbiddenIds,\n }),\n );\n useLayoutEffect(() => {\n store.set({ reparentingNodeId, reparentForbiddenIds });\n }, [store, reparentingNodeId, reparentForbiddenIds]);\n\n const latestProps = useRef(props);\n useLayoutEffect(() => {\n latestProps.current = props;\n });\n\n const callbacks = useMemo<OutlineCallbacks>(\n () => ({\n onNameClick: (event, node) => {\n // Keeps a name click from also toggling the enclosing <summary>'s section.\n event.preventDefault();\n const { reparentingNodeId: reparenting, onReparent, onNodeClick } = latestProps.current;\n if (reparenting) {\n void onReparent?.(reparenting, node.id);\n return;\n }\n const changed = store.get().selectedId !== node.id;\n store.set({ selectedId: node.id });\n if (changed) latestProps.current.onSelectedNodeChange?.(node);\n onNodeClick?.(node, event.nativeEvent);\n },\n onHover: (node) => latestProps.current.onNodeHover?.(node),\n toggleOpen: (key) => {\n const openIds = new Set(store.get().openIds);\n if (!openIds.delete(key)) openIds.add(key);\n store.set({ openIds });\n },\n onPlayPause: (nodeId) => latestProps.current.onPlayPause?.(nodeId),\n onAddChild: (parentId) => latestProps.current.onAddChild?.(parentId),\n onRenameRequest: (node) => latestProps.current.onRenameRequest?.(node),\n onDeleteRequest: (node) => latestProps.current.onDeleteRequest?.(node),\n onReparentRequest: (node) => latestProps.current.onReparentRequest?.(node),\n additionalActions: (node) => latestProps.current.additionalActions?.(node) ?? [],\n }),\n [store],\n );\n\n const selectedId = useOutlineState(store, (s) => s.selectedId);\n const pendingScrollIdRef = useRef<string | null>(null);\n\n // Expands every collapsed ancestor section of `nodeId`'s row, opens its info panel, and scrolls\n // it into view once rendered — shared by panel chip navigation and the externally-controlled\n // `selectedNodeId` (which passes `notify: false`, so it never echoes back). No-op for an id\n // that isn't in the tree.\n const selectNode = (nodeId: string, { notify = true }: { notify?: boolean } = {}) => {\n const ancestors = index.ancestorsOf(nodeId);\n if (!index.rootIdById.get(nodeId)) return;\n const openIds = new Set(store.get().openIds);\n for (const ancestor of ancestors) openIds.add(ancestor.id);\n const [root, branchChild = index.byId.get(nodeId)!] = ancestors;\n if (root && root.id !== model.centerRoot.id && !root.detached) {\n openIds.add(sectionKey(branchChild.side === \"pop\" ? \"pop\" : \"core\", root.id));\n }\n const changed = store.get().selectedId !== nodeId;\n if (changed) pendingScrollIdRef.current = nodeId;\n store.set({ openIds, selectedId: nodeId });\n if (changed && notify) latestProps.current.onSelectedNodeChange?.(index.byId.get(nodeId)!);\n };\n const selectNodeRef = useRef(selectNode);\n useLayoutEffect(() => {\n selectNodeRef.current = selectNode;\n });\n\n useLayoutEffect(() => {\n if (!selectedId || pendingScrollIdRef.current !== selectedId) return;\n pendingScrollIdRef.current = null;\n scrollRef.current\n ?.querySelector(`[data-gtv-node-id=\"${CSS.escape(selectedId)}\"]`)\n ?.scrollIntoView({ block: \"nearest\" });\n }, [selectedId]);\n\n useEffect(() => {\n if (!selectedNodeId || selectedId === selectedNodeId) return;\n selectNodeRef.current(selectedNodeId, { notify: false });\n }, [selectedNodeId, selectedId]);\n\n const prevSelectedNodeIdRef = useRef(selectedNodeId);\n useEffect(() => {\n if (prevSelectedNodeIdRef.current != null && selectedNodeId == null) store.set({ selectedId: null });\n prevSelectedNodeIdRef.current = selectedNodeId;\n }, [selectedNodeId, store]);\n\n const selectedNode = selectedId === null ? undefined : index.byId.get(selectedId);\n const itemProps = { model, store, callbacks };\n\n return (\n <div className={[\"gtv-outline\", selectedNode && \"gtv-outline--panel-open\", className].filter(Boolean).join(\" \")}>\n <div className=\"gtv-outline-scroll\" ref={scrollRef}>\n <ul className=\"gtv-outline-list gtv-outline-list--roots\">\n {[model.centerRoot, ...model.ringRoots].map((root) => (\n <OutlineNode key={root.id} nodeId={root.id} isRoot {...itemProps} />\n ))}\n </ul>\n {model.detachedRoots.length > 0 && (\n <ul className=\"gtv-outline-list gtv-outline-list--roots gtv-outline-list--detached\">\n {model.detachedRoots.map((root) => (\n <OutlineNode key={root.id} nodeId={root.id} isRoot {...itemProps} />\n ))}\n </ul>\n )}\n </div>\n\n {selectedNode && (\n <InfoPanel\n node={selectedNode}\n {...model.styleOf(selectedNode)}\n parentNode={(() => {\n const parent = index.byId.get(selectedNode.parentId ?? \"\");\n return parent ? { node: parent, ...model.styleOf(parent) } : null;\n })()}\n childNodes={(index.childrenByParentId.get(selectedNode.id) ?? []).map((n) => ({ node: n, ...model.styleOf(n) }))}\n ancestorNodes={index\n .ancestorsOf(selectedNode.id)\n .map((n): InfoPanelChild => ({ node: n, ...model.styleOf(n) }))}\n side=\"left\"\n onClose={() => {\n store.set({ selectedId: null });\n latestProps.current.onSelectedNodeChange?.(null);\n }}\n onSelectNode={(id) => selectNodeRef.current(id)}\n hideClose={hideInfoPanelClose}\n hideTitle={hideInfoPanelTitle}\n itemCountLabel={itemCountLabel}\n readOnly={readOnly}\n renderDangerZone={renderDangerZone}\n expanded={expanded}\n onExpandedChange={onExpandedChange}\n collapsed={collapsed}\n hideExpandButton={hideExpandButton}\n renderInfoPanelBody={renderInfoPanelBody}\n actions={\n showToolbar && !reparentingNodeId\n ? {\n playingNodeId,\n playState,\n onPlayPause: callbacks.onPlayPause,\n onAddChild: callbacks.onAddChild,\n onRenameRequest: callbacks.onRenameRequest,\n onDeleteRequest: callbacks.onDeleteRequest,\n onReparentRequest: callbacks.onReparentRequest,\n additionalActions: callbacks.additionalActions,\n }\n : undefined\n }\n />\n )}\n {overlayPanel && <OverlayPanel {...overlayPanel} />}\n </div>\n );\n}\n","\"use client\";\n\nimport { useId } from \"react\";\n\n// These values approximate the real GenreTree's visual tokens (card corner radius, surface\n// and connector colors) rather than importing them, since the @behindthemusictree/genre-tree-view\n// package only publicly exports GenreTree, getGenreTreeColor, and types — not its internal\n// dimension/style constants.\nconst CARD_FILL = \"#F4F4F5\";\nconst CARD_BORDER_COLOR = \"#E4E4E7\";\nconst CONNECTOR_COLOR = \"#D4D4D8\";\nconst CORNER_RADIUS = 8;\nconst SHIMMER_HIGHLIGHT_COLOR = \"#FFFFFF\";\n\n// A real genre tree can run several levels deep and fan out into dozens of subgenres, so the\n// skeleton mirrors that with 6 columns (root through 5 descendant levels) and a tapering node\n// count per level, rather than a token 1-3-2 shape, to read as \"a big tree is loading\" instead\n// of \"a few items are loading\".\nconst LEVEL_COUNTS = [1, 6, 13, 15, 20, 25];\nconst LEVEL_WIDTHS = [200, 180, 165, 150, 140, 130];\nconst LEVEL_HEIGHTS = [36, 26, 20, 17, 15, 14];\n\nconst LEAF_ROW_HEIGHT = 20;\nconst GROUP_GAP = 12;\nconst COLUMN_GAP = 120;\nconst PADDING_X = 16;\nconst PADDING_Y = 20;\n\nconst SHIMMER_BAND_WIDTH = 160;\n\ntype Card = { x: number; y: number; width: number; height: number };\ntype Range = { start: number; end: number };\n\nfunction cardCenterY(card: Card) {\n return card.y + card.height / 2;\n}\n\n// Spreads `childCount` nodes as evenly as possible across `parentCount` parents (remainder\n// distributed to the first parents), so level sizes that don't divide evenly (e.g. 13 into 6)\n// still produce a contiguous, evenly-ordered range of children per parent.\nfunction distributeChildren(parentCount: number, childCount: number): Range[] {\n const base = Math.floor(childCount / parentCount);\n const remainder = childCount % parentCount;\n const ranges: Range[] = [];\n let cursor = 0;\n for (let p = 0; p < parentCount; p++) {\n const count = base + (p < remainder ? 1 : 0);\n ranges.push({ start: cursor, end: cursor + count - 1 });\n cursor += count;\n }\n return ranges;\n}\n\n// CHILD_RANGES[i][n] gives the range of level (i+1) node indices that are children of the n-th\n// node at level i.\nconst CHILD_RANGES: Range[][] = [];\nfor (let i = 0; i < LEVEL_COUNTS.length - 1; i++) {\n CHILD_RANGES.push(distributeChildren(LEVEL_COUNTS[i], LEVEL_COUNTS[i + 1]));\n}\n\nconst LEVEL_X: number[] = [];\n{\n let x = PADDING_X;\n for (let i = 0; i < LEVEL_COUNTS.length; i++) {\n LEVEL_X.push(x);\n x += LEVEL_WIDTHS[i] + COLUMN_GAP;\n }\n}\n\nconst LAST_LEVEL = LEVEL_COUNTS.length - 1;\nconst LEVEL_CARDS: Card[][] = new Array(LEVEL_COUNTS.length);\nlet VIEWBOX_HEIGHT: number;\n\n// Leaf level is laid out first (top to bottom, with an extra gap between sibling groups), then\n// every level above it is positioned from the vertical midpoint of its children, walking up to\n// the root.\n{\n const leafCards: Card[] = [];\n let cursorY = PADDING_Y;\n for (const range of CHILD_RANGES[LAST_LEVEL - 1]) {\n for (let i = range.start; i <= range.end; i++) {\n leafCards.push({\n x: LEVEL_X[LAST_LEVEL],\n y: cursorY,\n width: LEVEL_WIDTHS[LAST_LEVEL],\n height: LEVEL_HEIGHTS[LAST_LEVEL],\n });\n cursorY += LEAF_ROW_HEIGHT;\n }\n cursorY += GROUP_GAP;\n }\n LEVEL_CARDS[LAST_LEVEL] = leafCards;\n VIEWBOX_HEIGHT = cursorY - GROUP_GAP + PADDING_Y;\n}\n\nfor (let i = LAST_LEVEL - 1; i >= 0; i--) {\n const childCards = LEVEL_CARDS[i + 1];\n LEVEL_CARDS[i] = CHILD_RANGES[i].map(({ start, end }) => {\n const centerY =\n (cardCenterY(childCards[start]) + cardCenterY(childCards[end])) / 2;\n return {\n x: LEVEL_X[i],\n y: centerY - LEVEL_HEIGHTS[i] / 2,\n width: LEVEL_WIDTHS[i],\n height: LEVEL_HEIGHTS[i],\n };\n });\n}\n\nconst ALL_CARDS: { card: Card; rootAccent?: boolean }[] = LEVEL_CARDS.flatMap(\n (cards, level) =>\n cards.map((card) => ({ card, rootAccent: level === 0 })),\n);\n\nconst ALL_LINKS: { from: Card; to: Card }[] = [];\nfor (let i = 0; i < LAST_LEVEL; i++) {\n const parentCards = LEVEL_CARDS[i];\n const childCards = LEVEL_CARDS[i + 1];\n CHILD_RANGES[i].forEach(({ start, end }, parentIndex) => {\n for (let c = start; c <= end; c++) {\n ALL_LINKS.push({ from: parentCards[parentIndex], to: childCards[c] });\n }\n });\n}\n\nconst VIEWBOX_WIDTH = LEVEL_X[LAST_LEVEL] + LEVEL_WIDTHS[LAST_LEVEL] + PADDING_X;\n\nfunction cardCenterLeft(card: Card) {\n return { x: card.x, y: card.y + card.height / 2 };\n}\n\nfunction cardCenterRight(card: Card) {\n return { x: card.x + card.width, y: card.y + card.height / 2 };\n}\n\nfunction connectorPath(from: Card, to: Card) {\n const start = cardCenterRight(from);\n const end = cardCenterLeft(to);\n const midX = (start.x + end.x) / 2;\n return `M${start.x},${start.y} C${midX},${start.y} ${midX},${end.y} ${end.x},${end.y}`;\n}\n\nfunction SkeletonCard({\n card,\n rootAccent,\n}: {\n card: Card;\n rootAccent?: boolean;\n}) {\n return (\n <>\n <rect\n x={card.x}\n y={card.y}\n width={card.width}\n height={card.height}\n rx={CORNER_RADIUS}\n ry={CORNER_RADIUS}\n fill={CARD_FILL}\n stroke={CARD_BORDER_COLOR}\n strokeWidth={rootAccent ? 1.5 : 1}\n />\n {rootAccent && (\n <circle\n cx={card.x + 18}\n cy={card.y + card.height / 2}\n r={5}\n fill={CONNECTOR_COLOR}\n />\n )}\n </>\n );\n}\n\nexport function GenreTreeSkeleton() {\n // Unique per mount so multiple skeletons on one page don't collide on <defs> ids.\n const uid = useId().replace(/[^a-zA-Z0-9]/g, \"\");\n const gradientId = `genre-tree-skeleton-gradient-${uid}`;\n const maskId = `genre-tree-skeleton-mask-${uid}`;\n const animationName = `genre-tree-skeleton-sweep-${uid}`;\n const shimmerBandClass = `genre-tree-skeleton-shimmer-band-${uid}`;\n const sweepDistance = VIEWBOX_WIDTH + SHIMMER_BAND_WIDTH;\n\n return (\n <div className=\"gtv-skeleton\">\n <span className=\"gtv-skeleton-sr-only\">Loading genre tree…</span>\n <svg\n viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}\n width={VIEWBOX_WIDTH}\n height={VIEWBOX_HEIGHT}\n className=\"gtv-skeleton-svg\"\n aria-hidden=\"true\"\n >\n <style>{`\n @keyframes ${animationName} {\n from { transform: translateX(0); }\n to { transform: translateX(${sweepDistance}px); }\n }\n .${shimmerBandClass} {\n animation: ${animationName} 1.5s linear infinite;\n }\n @media (prefers-reduced-motion: reduce) {\n .${shimmerBandClass} {\n animation: none;\n }\n }\n `}</style>\n <defs>\n <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n <stop\n offset=\"0%\"\n stopColor={SHIMMER_HIGHLIGHT_COLOR}\n stopOpacity={0}\n />\n <stop\n offset=\"50%\"\n stopColor={SHIMMER_HIGHLIGHT_COLOR}\n stopOpacity={0.9}\n />\n <stop\n offset=\"100%\"\n stopColor={SHIMMER_HIGHLIGHT_COLOR}\n stopOpacity={0}\n />\n </linearGradient>\n {/* White = visible: masks the shimmer band to the tree's own silhouette so the sweep\n only lights up cards and connectors, never the empty space between them. */}\n <mask id={maskId}>\n {ALL_LINKS.map((link, i) => (\n <path\n key={`link-mask-${i}`}\n d={connectorPath(link.from, link.to)}\n fill=\"none\"\n stroke=\"#FFFFFF\"\n strokeWidth={1.25}\n />\n ))}\n {ALL_CARDS.map(({ card }, i) => (\n <rect\n key={`card-mask-${i}`}\n x={card.x}\n y={card.y}\n width={card.width}\n height={card.height}\n rx={CORNER_RADIUS}\n ry={CORNER_RADIUS}\n fill=\"#FFFFFF\"\n />\n ))}\n </mask>\n </defs>\n\n {ALL_LINKS.map((link, i) => (\n <path\n key={`link-${i}`}\n d={connectorPath(link.from, link.to)}\n fill=\"none\"\n stroke={CONNECTOR_COLOR}\n strokeWidth={1.25}\n />\n ))}\n {ALL_CARDS.map(({ card, rootAccent }, i) => (\n <SkeletonCard key={`card-${i}`} card={card} rootAccent={rootAccent} />\n ))}\n\n <g mask={`url(#${maskId})`}>\n <rect\n className={shimmerBandClass}\n x={-SHIMMER_BAND_WIDTH}\n y={0}\n width={SHIMMER_BAND_WIDTH}\n height={VIEWBOX_HEIGHT}\n fill={`url(#${gradientId})`}\n />\n </g>\n </svg>\n </div>\n );\n}\n","\"use client\";\n\nimport { useId, useLayoutEffect, useRef } from \"react\";\nimport { NODE_DOT_MIN_RADIUS, TREE_COLORS } from \"./constants\";\n\n// Neutral grays rather than the real wheel's per-root tints, so the loading state reads as a\n// placeholder — see GenreTreeSkeleton's comment on why these are approximated, not imported.\nconst SHAPE_COLOR = \"#D4D4D8\";\nconst SECTOR_FILL_OPACITY = 0.22;\nconst INNER_TINT_OPACITY = 0.55;\nconst POP_LINK_OPACITY = 0.6;\nconst CANVAS_PADDING = 20;\n\nconst N_ROOTS = 9;\n\n// Radii (from the wheel's center) mirror the real pop/core wheel: a small mainstream subtree at\n// the center, the ring roots on the circle bounding the white \"pop\" region, each root's pop\n// branch reaching inward and its core branch outward.\nconst MAINSTREAM_RADIUS = 70;\nconst RING_RADIUS = 300;\nconst POP_RADII = [245, 195, 145];\nconst CORE_RADII = [375, 445, 515, 575];\n\nconst ROOT_DOT_RADIUS_RANGE = 8;\nconst LEVEL1_DOT_RADIUS_RANGE = 3;\nconst LINK_WIDTH = 1;\nconst LEAF_LINK_WIDTH = 0.75;\n\n// Angles use the real wheel's convention (0 = top, clockwise), so the sector gradient below\n// lines up with the dots without any translation.\ntype Point = { x: number; y: number };\ntype Polar = { angle: number; radius: number; point: Point };\ntype Dot = { point: Point; r: number };\ntype Link = { d: string; width: number; opacity: number };\n\n// Math.sin/Math.cos aren't specified to the ULP by ECMAScript, so raw trig output can differ in\n// the last float digit between server (Node) and client (browser) V8. Rounding well below that\n// noise floor makes every coordinate byte-identical across environments, so this can safely SSR.\nconst COORDINATE_PRECISION = 1000;\n\nfunction round(value: number): number {\n return Math.round(value * COORDINATE_PRECISION) / COORDINATE_PRECISION;\n}\n\nfunction pointOnCircle(angleDeg: number, radius: number): Point {\n const rad = (angleDeg * Math.PI) / 180;\n return { x: round(radius * Math.sin(rad)), y: round(-radius * Math.cos(rad)) };\n}\n\n// Deterministic (not Math.random()) so server and client renders stay identical.\nfunction unitRandom(seed: number): number {\n const x = Math.sin(seed * 12.9898) * 43758.5453;\n return round(x - Math.floor(x));\n}\n\nfunction randomCount(seed: number, min: number, max: number): number {\n return Math.min(max, min + Math.floor(unitRandom(seed) * (max - min + 1)));\n}\n\nfunction fanAngles(center: number, spread: number, count: number): number[] {\n if (count <= 1) return [center];\n const step = spread / (count - 1);\n return Array.from({ length: count }, (_, i) => center - spread / 2 + i * step);\n}\n\n// Same shape as d3.linkRadial: a cubic whose control points sit at the mid radius on each end's\n// own angle, so branches curve along the wheel's rings instead of cutting chords across them.\nfunction linkPath(from: Polar, to: Polar): string {\n const midRadius = (from.radius + to.radius) / 2;\n const c0 = pointOnCircle(from.angle, midRadius);\n const c1 = pointOnCircle(to.angle, midRadius);\n return `M${from.point.x},${from.point.y} C${c0.x},${c0.y} ${c1.x},${c1.y} ${to.point.x},${to.point.y}`;\n}\n\nconst DOTS: Dot[] = [];\nconst LINKS: Link[] = [];\n\nfunction addNode(angle: number, radius: number, dotRadius: number): Polar {\n const point = pointOnCircle(angle, radius);\n DOTS.push({ point, r: dotRadius });\n return { angle, radius, point };\n}\n\nfunction addLink(from: Polar, to: Polar, width: number, opacity = 1) {\n LINKS.push({ d: linkPath(from, to), width, opacity });\n}\n\n// Roots share the circle unevenly, like the real wheel's weight-proportional sectors; each root\n// sits at the middle of its own span and its branches fan within it.\nconst ROOT_WEIGHTS = Array.from({ length: N_ROOTS }, (_, i) => 0.25 + 0.75 * unitRandom(i * 17 + 3));\nconst TOTAL_WEIGHT = ROOT_WEIGHTS.reduce((sum, weight) => sum + weight, 0);\n\ntype Sector = { startAngle: number; endAngle: number; color: string };\nconst SECTORS: Sector[] = [];\n{\n let cursor = (-360 * ROOT_WEIGHTS[0]) / TOTAL_WEIGHT / 2;\n ROOT_WEIGHTS.forEach((weight, i) => {\n const span = (360 * weight) / TOTAL_WEIGHT;\n SECTORS.push({ startAngle: cursor, endAngle: cursor + span, color: TREE_COLORS[i % TREE_COLORS.length] });\n cursor += span;\n });\n}\n\nfunction buildMainstream() {\n const center = addNode(0, 0, NODE_DOT_MIN_RADIUS * 2);\n const level1Count = 6;\n fanAngles(0, 360 - 360 / level1Count, level1Count).forEach((angle, i) => {\n const level1 = addNode(angle, 26, NODE_DOT_MIN_RADIUS * 0.8);\n addLink(center, level1, LINK_WIDTH);\n const leafCount = randomCount(i * 5 + 101, 2, 3);\n fanAngles(angle, 40, leafCount).forEach((leafAngle) => {\n addLink(level1, addNode(leafAngle, 50, NODE_DOT_MIN_RADIUS * 0.5), LEAF_LINK_WIDTH);\n });\n });\n}\n\nfunction buildRoot(index: number) {\n const { startAngle, endAngle } = SECTORS[index];\n const angle = (startAngle + endAngle) / 2;\n const usable = (endAngle - startAngle) * 0.8;\n const weight = ROOT_WEIGHTS[index];\n const leafDotRadius = NODE_DOT_MIN_RADIUS * 0.6;\n\n const root = addNode(angle, RING_RADIUS, NODE_DOT_MIN_RADIUS + weight * ROOT_DOT_RADIUS_RANGE);\n\n const popLevel1 = addNode(angle, POP_RADII[0], NODE_DOT_MIN_RADIUS + weight * LEVEL1_DOT_RADIUS_RANGE * 0.6);\n addLink(root, popLevel1, LINK_WIDTH, POP_LINK_OPACITY);\n const popLevel2Count = 2 + Math.round(weight * 2);\n fanAngles(angle, usable * 0.5, popLevel2Count).forEach((level2Angle, i) => {\n const popLevel2 = addNode(level2Angle, POP_RADII[1], NODE_DOT_MIN_RADIUS * 0.8);\n addLink(popLevel1, popLevel2, LINK_WIDTH, POP_LINK_OPACITY);\n const leafCount = randomCount(index * 31 + i * 7 + 211, 2, 3);\n fanAngles(level2Angle, (usable * 0.5) / popLevel2Count, leafCount).forEach((leafAngle) => {\n addLink(popLevel2, addNode(leafAngle, POP_RADII[2], leafDotRadius), LEAF_LINK_WIDTH, POP_LINK_OPACITY);\n });\n });\n\n const coreLevel1 = addNode(angle, CORE_RADII[0], NODE_DOT_MIN_RADIUS + weight * LEVEL1_DOT_RADIUS_RANGE);\n addLink(root, coreLevel1, LINK_WIDTH);\n const coreLevel2Count = 2 + Math.round(weight * 5);\n const coreLevel2Spread = usable * 0.75;\n const level2Slot = coreLevel2Spread / coreLevel2Count;\n fanAngles(angle, coreLevel2Spread, coreLevel2Count).forEach((level2Angle, i) => {\n const coreLevel2 = addNode(level2Angle, CORE_RADII[1], NODE_DOT_MIN_RADIUS * 0.9);\n addLink(coreLevel1, coreLevel2, LINK_WIDTH);\n const level3Count = randomCount(index * 31 + i * 7 + 1, 2, 5);\n fanAngles(level2Angle, level2Slot * 0.9, level3Count).forEach((level3Angle, j) => {\n const coreLevel3 = addNode(level3Angle, CORE_RADII[2], leafDotRadius);\n addLink(coreLevel2, coreLevel3, LEAF_LINK_WIDTH);\n if (weight < 0.75) return;\n const level4Count = randomCount(index * 131 + i * 17 + j * 3 + 401, 1, 2);\n fanAngles(level3Angle, (level2Slot * 0.9) / level3Count, level4Count).forEach((level4Angle) => {\n addLink(coreLevel3, addNode(level4Angle, CORE_RADII[3], leafDotRadius * 0.8), LEAF_LINK_WIDTH);\n });\n });\n });\n}\n\nbuildMainstream();\nfor (let i = 0; i < N_ROOTS; i++) buildRoot(i);\n\n// Square and centered on the wheel's pivot (like the real wheel) so the shimmer's rotation\n// origin stays at the viewBox's own middle.\nconst CONTENT_RADIUS = Math.max(\n RING_RADIUS,\n ...DOTS.map((dot) => Math.hypot(dot.point.x, dot.point.y) + dot.r),\n);\nconst CANVAS_SIZE = (CONTENT_RADIUS + CANVAS_PADDING) * 2;\nconst CANVAS_ORIGIN = -(CONTENT_RADIUS + CANVAS_PADDING);\n\n// Rendered as a CSS conic-gradient on a layer separate from the content SVG (see the component\n// below) rather than as SVG wedge paths sharing the content's own viewBox: the content SVG uses\n// `preserveAspectRatio=\"xMidYMid meet\"` so the wheel itself is never cropped, but \"meet\" letterboxes\n// a square viewBox inside a non-square container — a wedge drawn in that same coordinate space\n// would letterbox too, leaving the container's corners uncolored. A CSS gradient instead covers\n// `100% / 100%` of the container directly, independent of the content's aspect ratio.\n// CSS conic-gradient's 0deg (12 o'clock, clockwise) matches this file's own angle convention, so\n// stops need only be shifted so the first one starts at 0deg.\nconst SECTOR_GRADIENT_ROTATION = SECTORS[0].startAngle;\nconst SECTOR_GRADIENT_CSS = `conic-gradient(from ${SECTOR_GRADIENT_ROTATION}deg, ${SECTORS.map(\n (sector) =>\n `${sector.color} ${sector.startAngle - SECTOR_GRADIENT_ROTATION}deg ${\n sector.endAngle - SECTOR_GRADIENT_ROTATION\n }deg`,\n).join(\", \")})`;\n\n// The sweep is an HTML layer (rotating on the compositor) clipped by a static mask, instead of\n// an SVG <mask> re-rasterized every frame: SVG children can't be composited, so animating a wedge\n// under an SVG mask repaints hundreds of shapes per frame on the main thread, which is busiest\n// exactly while the skeleton is shown. The mask ships as wheel-skeleton-mask.svg (referenced from\n// styles.css, snapshot-tested against this function) so it applies from the first paint.\nexport function buildMaskSvgMarkup(): string {\n const links = LINKS.map(\n (link) =>\n `<path d=\"${link.d}\" fill=\"none\" stroke=\"#fff\" stroke-width=\"${link.width}\" stroke-opacity=\"${link.opacity}\" stroke-linecap=\"round\"/>`,\n ).join(\"\");\n const dots = DOTS.map((dot) => `<circle cx=\"${dot.point.x}\" cy=\"${dot.point.y}\" r=\"${dot.r}\" fill=\"#fff\"/>`).join(\"\");\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"${CANVAS_ORIGIN} ${CANVAS_ORIGIN} ${CANVAS_SIZE} ${CANVAS_SIZE}\">${links}${dots}</svg>`;\n}\n\n// Start time of the first skeleton's spin, shared so every later remount (one per loading stage)\n// continues at the same angle instead of restarting from 0deg.\nlet sharedStartTime: CSSNumberish | null = null;\n\nexport const WHEEL_SKELETON_SPIN_ANIMATION = \"gtv-wheel-skeleton-spin\";\n\nfunction adoptRunningSweepStartTime(): void {\n if (sharedStartTime !== null || typeof document === \"undefined\") return;\n const running = document.getAnimations?.().find(\n (animation) => (animation as CSSAnimation).animationName === WHEEL_SKELETON_SPIN_ANIMATION,\n );\n sharedStartTime = running?.startTime ?? null;\n}\n\nexport function GenreTreeWheelSkeleton() {\n // Read during render: a server-rendered skeleton discarded at hydration is already out of the\n // document by layout-effect time, and the replacement must continue its angle.\n adoptRunningSweepStartTime();\n // Unique per mount so multiple skeletons on one page don't collide on <defs> ids.\n const uid = useId().replace(/[^a-zA-Z0-9]/g, \"\");\n const shapesId = `genre-tree-wheel-skeleton-shapes-${uid}`;\n const mainstreamId = `genre-tree-wheel-skeleton-mainstream-${uid}`;\n const wedgeRef = useRef<HTMLDivElement>(null);\n\n // The mask and the spin come from CSS so the server-rendered skeleton is already correct before\n // hydration; this only syncs the phase. No animation means reduced motion or no WAAPI.\n useLayoutEffect(() => {\n const [animation] = wedgeRef.current?.getAnimations?.() ?? [];\n if (!animation) return;\n sharedStartTime ??= animation.startTime ?? document.timeline.currentTime;\n animation.startTime = sharedStartTime;\n }, []);\n\n return (\n <div className=\"gtv-wheel-skeleton\">\n <div\n className=\"gtv-wheel-skeleton-sector-layer\"\n style={{ background: SECTOR_GRADIENT_CSS, opacity: SECTOR_FILL_OPACITY }}\n aria-hidden=\"true\"\n />\n <span className=\"gtv-skeleton-sr-only\">Loading genre tree…</span>\n <svg\n viewBox={`${CANVAS_ORIGIN} ${CANVAS_ORIGIN} ${CANVAS_SIZE} ${CANVAS_SIZE}`}\n preserveAspectRatio=\"xMidYMid meet\"\n className=\"gtv-wheel-skeleton-svg\"\n aria-hidden=\"true\"\n >\n <defs>\n <radialGradient id={mainstreamId}>\n <stop offset=\"0%\" stopColor=\"#FFFFFF\" stopOpacity={0.45} />\n <stop offset=\"65%\" stopColor=\"#FFFFFF\" stopOpacity={0.15} />\n <stop offset=\"100%\" stopColor=\"#FFFFFF\" stopOpacity={0} />\n </radialGradient>\n <g id={shapesId}>\n {LINKS.map((link, i) => (\n <path\n key={`link-${i}`}\n d={link.d}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={link.width}\n strokeOpacity={link.opacity}\n strokeLinecap=\"round\"\n />\n ))}\n {DOTS.map((dot, i) => (\n <circle key={`dot-${i}`} cx={dot.point.x} cy={dot.point.y} r={dot.r} fill=\"currentColor\" />\n ))}\n </g>\n </defs>\n\n <circle cx={0} cy={0} r={RING_RADIUS} fill=\"#FFFFFF\" fillOpacity={INNER_TINT_OPACITY} />\n <circle cx={0} cy={0} r={MAINSTREAM_RADIUS} fill={`url(#${mainstreamId})`} />\n\n <use href={`#${shapesId}`} color={SHAPE_COLOR} />\n </svg>\n <div className=\"gtv-wheel-skeleton-shimmer\" aria-hidden=\"true\">\n <div ref={wedgeRef} className=\"gtv-wheel-skeleton-shimmer-wedge\" />\n </div>\n </div>\n );\n}\n","\"use client\";\n\n// Indent level and relative width per placeholder row — a few roots, one expanded, so it reads\n// as a nested outline rather than a flat list.\nconst ROWS: { depth: number; width: number }[] = [\n { depth: 0, width: 38 },\n { depth: 0, width: 26 },\n { depth: 1, width: 18 },\n { depth: 2, width: 30 },\n { depth: 2, width: 24 },\n { depth: 1, width: 16 },\n { depth: 0, width: 32 },\n { depth: 0, width: 22 },\n];\n\nexport function GenreTreeOutlineSkeleton() {\n return (\n <div className=\"gtv-skeleton gtv-outline-skeleton\">\n <span className=\"gtv-skeleton-sr-only\">Loading genre tree…</span>\n {ROWS.map((row, i) => (\n <div\n key={i}\n className=\"gtv-outline-skeleton-row\"\n aria-hidden=\"true\"\n style={{ marginLeft: `${row.depth * 1.25}rem`, width: `${row.width}%` }}\n />\n ))}\n </div>\n );\n}\n","\"use client\";\n\nimport type { GenreTreeViewMode } from \"./types\";\nimport { GenreTreeSkeleton } from \"./GenreTreeSkeleton\";\nimport { GenreTreeWheelSkeleton } from \"./GenreTreeWheelSkeleton\";\nimport { GenreTreeOutlineSkeleton } from \"./GenreTreeOutlineSkeleton\";\n\nexport type GenreTreeViewSkeletonProps = {\n viewMode: GenreTreeViewMode;\n};\n\n// Single source of truth for viewMode -> skeleton shape, so a consumer's next/dynamic\n// `loading` fallback (rendered before GenreTreeView's own chunk downloads and this logic\n// can run) never disagrees with what GenreTreeView renders internally once mounted.\nexport function GenreTreeViewSkeleton({ viewMode }: GenreTreeViewSkeletonProps) {\n if (viewMode === \"wheel\" || viewMode === \"pop-core\") {\n return (\n <div className=\"gtv-view-skeleton-wheel-wrapper\">\n <GenreTreeWheelSkeleton />\n </div>\n );\n }\n if (viewMode === \"outline\") return <GenreTreeOutlineSkeleton />;\n return <GenreTreeSkeleton />;\n}\n"],"mappings":"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","names":["import_react","ReactDOMServer","svgHeight","svgWidth","import_jsx_runtime","import_jsx_runtime","import_react","import_react","import_react","import_md","import_jsx_runtime","import_react","import_md","import_jsx_runtime","import_react","import_md","import_pi","import_jsx_runtime","import_jsx_runtime","itemCount","fill","import_react","import_md","import_jsx_runtime","import_jsx_runtime","import_react","d3","CHIP_GAP","import_jsx_runtime","d3","import_jsx_runtime","import_jsx_runtime","import_react","d3","sum","CHIP_GAP","right","collectSubtree","import_jsx_runtime","index","d3","import_jsx_runtime","import_react","d3","import_react","import_react","import_md","import_jsx_runtime","centerNodes","import_jsx_runtime","LANDING_ANGLE","computeContinuousAngles","d3","index","import_jsx_runtime","import_react","import_jsx_runtime","OutlineSection","import_react","import_jsx_runtime","CONNECTOR_COLOR","CORNER_RADIUS","import_react","import_jsx_runtime","sum","import_jsx_runtime","import_jsx_runtime"]}