{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "smart-edge",
"title": "Smart Edge",
"description": "A React Flow v12 edge that auto-connects to the nearest sides of its source and target nodes.",
"dependencies": ["@xyflow/react", "clsx", "tailwind-merge", "lucide-react"],
"registryDependencies": ["edge-geometry", "base-node"],
"files": [
{
"path": "src/registry/thornberry/components/smart-edge.tsx",
"content": "\"use client\";\n\nimport { getSmoothStepPath, useStore } from \"@xyflow/react\";\nimport { memo, useMemo } from \"react\";\n\nimport {\n NODE_MIN_HEIGHT,\n NODE_WIDTH,\n} from \"@/registry/thornberry/components/base-node\";\nimport {\n EDGE_OFFSET,\n getBestConnectionPoints,\n} from \"@/registry/thornberry/lib/edge-geometry\";\n\nimport type { EdgeProps, ReactFlowState } from \"@xyflow/react\";\nimport type { NodeRect } from \"@/registry/thornberry/lib/edge-geometry\";\n\n/** Edge styling constants */\nconst EDGE_STROKE_WIDTH = 3;\n\ninterface EdgeNodeRects {\n source: NodeRect | null;\n target: NodeRect | null;\n}\n\n/**\n * Build a memoized selector that reads the absolute rect of the source\n * and target nodes from the v12 store.\n *\n * In @xyflow/react v12 nodes live in `state.nodeLookup` (a Map of\n * `InternalNode`). Absolute position comes from\n * `internals.positionAbsolute` and measured size from `measured`,\n * falling back to the node's relative `position` and the default\n * dimensions when a node has not been measured yet.\n */\nconst createEdgePositionsSelector = (sourceId: string, targetId: string) => {\n let prevResult: EdgeNodeRects | null = null;\n\n return (state: ReactFlowState): EdgeNodeRects => {\n const sourceNode = state.nodeLookup.get(sourceId);\n const targetNode = state.nodeLookup.get(targetId);\n\n const source: NodeRect | null = sourceNode\n ? {\n x: sourceNode.internals.positionAbsolute.x,\n y: sourceNode.internals.positionAbsolute.y,\n width: sourceNode.measured.width ?? NODE_WIDTH,\n height: sourceNode.measured.height ?? NODE_MIN_HEIGHT,\n }\n : null;\n\n const target: NodeRect | null = targetNode\n ? {\n x: targetNode.internals.positionAbsolute.x,\n y: targetNode.internals.positionAbsolute.y,\n width: targetNode.measured.width ?? NODE_WIDTH,\n height: targetNode.measured.height ?? NODE_MIN_HEIGHT,\n }\n : null;\n\n // Preserve referential equality when positions are unchanged\n if (\n prevResult &&\n prevResult.source?.x === source?.x &&\n prevResult.source?.y === source?.y &&\n prevResult.target?.x === target?.x &&\n prevResult.target?.y === target?.y\n ) {\n return prevResult;\n }\n\n prevResult = { source, target };\n return prevResult;\n };\n};\n\n/**\n * SmartEdge automatically connects to the nearest sides of its source\n * and target nodes, routing with a smooth step path.\n */\nexport const SmartEdge = memo(\n ({\n id,\n source,\n target,\n sourceX: fallbackSourceX,\n sourceY: fallbackSourceY,\n targetX: fallbackTargetX,\n targetY: fallbackTargetY,\n sourcePosition: fallbackSourcePosition,\n targetPosition: fallbackTargetPosition,\n style = {},\n markerEnd,\n label,\n labelStyle,\n labelShowBg = true,\n labelBgStyle,\n selected,\n }: EdgeProps) => {\n const selector = useMemo(\n () => createEdgePositionsSelector(source, target),\n [source, target],\n );\n const { source: sourceRect, target: targetRect } = useStore(selector);\n\n let path: string;\n let labelX: number;\n let labelY: number;\n\n if (!sourceRect || !targetRect) {\n // Fall back to the coordinates React Flow provides\n [path, labelX, labelY] = getSmoothStepPath({\n sourceX: fallbackSourceX,\n sourceY: fallbackSourceY,\n sourcePosition: fallbackSourcePosition,\n targetX: fallbackTargetX,\n targetY: fallbackTargetY,\n targetPosition: fallbackTargetPosition,\n borderRadius: 0,\n offset: EDGE_OFFSET,\n });\n } else {\n const {\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n } = getBestConnectionPoints(sourceRect, targetRect);\n\n [path, labelX, labelY] = getSmoothStepPath({\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n borderRadius: 0,\n offset: EDGE_OFFSET,\n });\n }\n\n const strokeColor = (style.stroke as string) || \"currentColor\";\n\n return (\n \n {/* Invisible wider path for easier selection */}\n \n\n {/* Glow effect for the selected state */}\n {selected && (\n \n )}\n\n {/* Main edge path */}\n \n\n {/* Optional label */}\n {label && (\n \n
\n
\n \n {label}\n \n
\n
\n \n )}\n \n );\n },\n);\n\nSmartEdge.displayName = \"SmartEdge\";\n",
"type": "registry:ui"
},
{
"path": "src/registry/thornberry/components/base-node.tsx",
"content": "\"use client\";\n\nimport { Handle } from \"@xyflow/react\";\nimport { memo } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { resolveTheme } from \"@/registry/thornberry/lib/node-themes\";\n\nimport type { Node, NodeProps, Position } from \"@xyflow/react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport type {\n NodeTheme,\n NodeThemeKey,\n} from \"@/registry/thornberry/lib/node-themes\";\n\n/**\n * Standard node dimensions for consistent canvas layout. The width is an\n * even multiple of the default grid size for clean center alignment.\n */\nexport const NODE_WIDTH = 300;\nexport const NODE_MIN_HEIGHT = 120;\n\n/** Connection handle configuration */\ninterface NodeHandleConfig {\n type: \"source\" | \"target\";\n position: Position;\n id?: string;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * Node data shape consumed by the `base` node type. React Flow supplies\n * `selected` on the node props, so it is not part of the data payload.\n * Consumers add nodes as `{ type: \"base\", data: { theme, label, ... } }`.\n */\nexport interface BaseNodeData {\n /** Theme key or fully custom theme object */\n theme: NodeThemeKey | NodeTheme;\n /** Optional icon rendered in the header badge */\n icon?: LucideIcon;\n /** Node title */\n label: string;\n /** Optional secondary description under the label */\n description?: string;\n /** Optional badge text rendered in the header */\n badge?: string;\n /** Connection handles to render */\n handles?: NodeHandleConfig[];\n /** Optional body content */\n children?: ReactNode;\n /** Optional footer content */\n footer?: ReactNode;\n /** Click handler for the node */\n onClick?: () => void;\n /** Delete handler; when set, a delete button is shown on hover */\n onDelete?: () => void;\n // Index signature so the shape satisfies React Flow's `Record`-based node data\n [key: string]: unknown;\n}\n\n/**\n * Generic base node providing a consistent header (icon, label, optional\n * description and badge), optional body and footer, selection state, and\n * connection handles. It is a proper React Flow node component: content\n * comes from `data` and selection from props, so it can be registered\n * directly as a `nodeType` without an adapter. Styling is driven by a\n * semantic {@link NodeTheme}.\n */\nexport const BaseNode = memo(\n ({ data, selected }: NodeProps>) => {\n const {\n theme,\n icon: Icon,\n label,\n description,\n badge,\n handles = [],\n children,\n footer,\n onClick,\n onDelete,\n } = data;\n const t = resolveTheme(theme);\n\n return (\n