{ "name": "circuit-board", "type": "registry:ui", "dependencies": [ "framer-motion" ], "registryDependencies": [], "description": "An interactive circuit board layout component with animated electricity paths that pulse between connected nodes. Fully theme-aware with automatic light/dark mode support.", "files": [ { "path": "components/ui/circuit-board.tsx", "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion } from \"framer-motion\"\nimport { cn } from \"@workspace/ui/lib/utils\"\n\ninterface CircuitNode {\n id: string\n x: number\n y: number\n label?: string\n icon?: React.ReactNode\n status?: \"active\" | \"inactive\" | \"processing\" | \"error\"\n size?: \"sm\" | \"md\" | \"lg\"\n}\n\ninterface CircuitConnection {\n from: string\n to: string\n animated?: boolean\n bidirectional?: boolean\n color?: string\n pulseColor?: string\n}\n\ninterface CircuitBoardProps extends React.HTMLAttributes {\n nodes: CircuitNode[]\n connections: CircuitConnection[]\n width?: number\n height?: number\n gridSize?: number\n showGrid?: boolean\n gridColor?: string\n traceColor?: string\n pulseColor?: string\n nodeColor?: string\n pulseSpeed?: number\n traceWidth?: number\n /** Force a specific theme variant. Defaults to auto-detect from system. */\n variant?: \"light\" | \"dark\" | \"auto\"\n}\n\nfunction CircuitBoard({\n nodes,\n connections,\n width = 600,\n height = 400,\n gridSize = 20,\n showGrid = true,\n gridColor,\n traceColor,\n pulseColor,\n nodeColor,\n pulseSpeed = 2,\n traceWidth = 2,\n variant = \"auto\",\n className,\n ...props\n}: CircuitBoardProps) {\n // Theme-aware color defaults\n const [isDark, setIsDark] = React.useState(true)\n\n React.useEffect(() => {\n if (variant !== \"auto\") {\n setIsDark(variant === \"dark\")\n return\n }\n\n // Check for dark class on html/body\n const checkTheme = () => {\n const isDarkMode = document.documentElement.classList.contains(\"dark\") ||\n document.body.classList.contains(\"dark\")\n setIsDark(isDarkMode)\n }\n\n checkTheme()\n\n // Listen for changes\n const observer = new MutationObserver(checkTheme)\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] })\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] })\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\")\n mediaQuery.addEventListener(\"change\", checkTheme)\n\n return () => {\n observer.disconnect()\n mediaQuery.removeEventListener(\"change\", checkTheme)\n }\n }, [variant])\n\n // Compute theme-aware colors\n const computedGridColor = gridColor || (isDark ? \"rgba(163, 163, 163, 0.08)\" : \"rgba(64, 64, 64, 0.12)\")\n const computedTraceColor = traceColor || (isDark ? \"rgba(163, 163, 163, 0.25)\" : \"rgba(64, 64, 64, 0.35)\")\n const computedPulseColor = pulseColor || (isDark ? \"rgba(163, 163, 163, 0.6)\" : \"rgba(64, 64, 64, 0.7)\")\n const computedNodeColor = nodeColor || (isDark ? \"rgba(163, 163, 163, 0.5)\" : \"rgba(64, 64, 64, 0.6)\")\n const nodeMap = React.useMemo(() => {\n return new Map(nodes.map((node) => [node.id, node]))\n }, [nodes])\n\n const getNodeSize = React.useCallback((size?: CircuitNode[\"size\"]) => {\n switch (size) {\n case \"sm\":\n return 24\n case \"lg\":\n return 48\n default:\n return 36\n }\n }, [])\n\n const calculatePath = React.useCallback(\n (from: CircuitNode, to: CircuitNode): string => {\n const fromSize = getNodeSize(from.size) / 2 + 4\n const toSize = getNodeSize(to.size) / 2 + 4\n\n const dx = to.x - from.x\n const dy = to.y - from.y\n\n // Calculate start and end points offset from node centers\n let startX = from.x\n let startY = from.y\n let endX = to.x\n let endY = to.y\n\n // Create circuit-like paths with right angles\n if (Math.abs(dx) > Math.abs(dy)) {\n // Horizontal first, then vertical\n startX = from.x + (dx > 0 ? fromSize : -fromSize)\n endX = to.x + (dx > 0 ? -toSize : toSize)\n const midX = from.x + dx / 2\n return `M ${startX} ${startY} H ${midX} V ${endY} H ${endX}`\n } else {\n // Vertical first, then horizontal\n startY = from.y + (dy > 0 ? fromSize : -fromSize)\n endY = to.y + (dy > 0 ? -toSize : toSize)\n const midY = from.y + dy / 2\n return `M ${startX} ${startY} V ${midY} H ${endX} V ${endY}`\n }\n },\n [getNodeSize]\n )\n\n const getStatusColor = (status?: CircuitNode[\"status\"]) => {\n if (isDark) {\n switch (status) {\n case \"active\":\n return \"rgba(163, 163, 163, 0.7)\"\n case \"processing\":\n return \"rgba(163, 163, 163, 0.5)\"\n case \"error\":\n return \"rgba(120, 113, 108, 0.6)\"\n default:\n return computedNodeColor\n }\n } else {\n switch (status) {\n case \"active\":\n return \"rgba(64, 64, 64, 0.8)\"\n case \"processing\":\n return \"rgba(64, 64, 64, 0.6)\"\n case \"error\":\n return \"rgba(180, 83, 83, 0.7)\"\n default:\n return computedNodeColor\n }\n }\n }\n\n return (\n \n \n \n {/* Glow filter for the pulse effect */}\n \n \n \n \n \n \n \n\n {/* Grid pattern */}\n {showGrid && (\n \n \n \n )}\n\n {/* Animated gradient for electricity effect */}\n {connections.map((conn, i) => (\n \n \n \n \n \n \n \n ))}\n \n\n {/* Grid background */}\n {showGrid && (\n \n )}\n\n {/* Connection traces */}\n {connections.map((conn, i) => {\n const fromNode = nodeMap.get(conn.from)\n const toNode = nodeMap.get(conn.to)\n if (!fromNode || !toNode) return null\n\n const path = calculatePath(fromNode, toNode)\n const pathLength = 500 // Approximate path length for animation\n\n return (\n \n {/* Base trace */}\n \n\n {/* Animated electricity pulse */}\n {conn.animated !== false && (\n \n )}\n\n {/* Bidirectional pulse */}\n {conn.bidirectional && (\n \n )}\n\n\n \n )\n })}\n \n\n {/* Nodes */}\n {nodes.map((node, i) => {\n const size = getNodeSize(node.size)\n const statusColor = getStatusColor(node.status)\n\n return (\n \n {/* Node background with pulse */}\n \n\n {/* Node border */}\n \n\n {/* Inner glow for active nodes */}\n {node.status === \"active\" && (\n \n )}\n\n {/* Node content */}\n
\n {node.icon && (\n
{node.icon}
\n )}\n
\n\n {/* Label */}\n {node.label && (\n \n {node.label}\n \n )}\n \n )\n })}\n \n )\n}\n\n// Pre-built circuit patterns\ninterface CircuitPatternProps extends Omit {\n pattern: \"data-flow\" | \"network\" | \"processor\" | \"tree\"\n}\n\nfunction CircuitPattern({ pattern, ...props }: CircuitPatternProps) {\n const patterns = {\n \"data-flow\": {\n nodes: [\n { id: \"input\", x: 50, y: 200, label: \"Input\", status: \"active\" as const },\n { id: \"process1\", x: 200, y: 100, label: \"Process\", status: \"processing\" as const },\n { id: \"process2\", x: 200, y: 300, label: \"Validate\", status: \"active\" as const },\n { id: \"merge\", x: 400, y: 200, label: \"Merge\", status: \"active\" as const },\n { id: \"output\", x: 550, y: 200, label: \"Output\", status: \"active\" as const },\n ],\n connections: [\n { from: \"input\", to: \"process1\", animated: true },\n { from: \"input\", to: \"process2\", animated: true },\n { from: \"process1\", to: \"merge\", animated: true },\n { from: \"process2\", to: \"merge\", animated: true },\n { from: \"merge\", to: \"output\", animated: true },\n ],\n },\n network: {\n nodes: [\n { id: \"server\", x: 300, y: 80, label: \"Server\", status: \"active\" as const, size: \"lg\" as const },\n { id: \"client1\", x: 100, y: 200, label: \"Client 1\", status: \"active\" as const },\n { id: \"client2\", x: 300, y: 250, label: \"Client 2\", status: \"processing\" as const },\n { id: \"client3\", x: 500, y: 200, label: \"Client 3\", status: \"active\" as const },\n { id: \"db\", x: 300, y: 350, label: \"Database\", status: \"active\" as const },\n ],\n connections: [\n { from: \"server\", to: \"client1\", bidirectional: true },\n { from: \"server\", to: \"client2\", bidirectional: true },\n { from: \"server\", to: \"client3\", bidirectional: true },\n { from: \"server\", to: \"db\", bidirectional: true },\n ],\n },\n processor: {\n nodes: [\n { id: \"alu\", x: 300, y: 200, label: \"ALU\", status: \"processing\" as const, size: \"lg\" as const },\n { id: \"reg1\", x: 150, y: 100, label: \"R1\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"reg2\", x: 150, y: 200, label: \"R2\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"reg3\", x: 150, y: 300, label: \"R3\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"cache\", x: 450, y: 200, label: \"Cache\", status: \"active\" as const },\n { id: \"out\", x: 550, y: 200, label: \"Out\", status: \"active\" as const, size: \"sm\" as const },\n ],\n connections: [\n { from: \"reg1\", to: \"alu\", animated: true },\n { from: \"reg2\", to: \"alu\", animated: true },\n { from: \"reg3\", to: \"alu\", animated: true },\n { from: \"alu\", to: \"cache\", animated: true },\n { from: \"cache\", to: \"out\", animated: true },\n ],\n },\n tree: {\n nodes: [\n { id: \"root\", x: 300, y: 50, label: \"Root\", status: \"active\" as const },\n { id: \"l1\", x: 150, y: 150, label: \"L1\", status: \"active\" as const },\n { id: \"r1\", x: 450, y: 150, label: \"R1\", status: \"processing\" as const },\n { id: \"l1l\", x: 80, y: 280, label: \"L1L\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"l1r\", x: 220, y: 280, label: \"L1R\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"r1l\", x: 380, y: 280, label: \"R1L\", status: \"error\" as const, size: \"sm\" as const },\n { id: \"r1r\", x: 520, y: 280, label: \"R1R\", status: \"active\" as const, size: \"sm\" as const },\n ],\n connections: [\n { from: \"root\", to: \"l1\", animated: true },\n { from: \"root\", to: \"r1\", animated: true },\n { from: \"l1\", to: \"l1l\", animated: true },\n { from: \"l1\", to: \"l1r\", animated: true },\n { from: \"r1\", to: \"r1l\", animated: true },\n { from: \"r1\", to: \"r1r\", animated: true },\n ],\n },\n }\n\n const selectedPattern = patterns[pattern]\n return \n}\n\n// Interactive circuit node for building custom circuits\ninterface CircuitNodeComponentProps {\n status?: \"active\" | \"inactive\" | \"processing\" | \"error\"\n size?: \"sm\" | \"md\" | \"lg\"\n glowColor?: string\n children?: React.ReactNode\n className?: string\n onClick?: () => void\n}\n\nfunction CircuitNode({\n status = \"inactive\",\n size = \"md\",\n glowColor,\n children,\n className,\n onClick,\n}: CircuitNodeComponentProps) {\n const [isDark, setIsDark] = React.useState(true)\n\n React.useEffect(() => {\n const checkTheme = () => {\n const isDarkMode = document.documentElement.classList.contains(\"dark\") ||\n document.body.classList.contains(\"dark\")\n setIsDark(isDarkMode)\n }\n\n checkTheme()\n\n const observer = new MutationObserver(checkTheme)\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] })\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] })\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\")\n mediaQuery.addEventListener(\"change\", checkTheme)\n\n return () => {\n observer.disconnect()\n mediaQuery.removeEventListener(\"change\", checkTheme)\n }\n }, [])\n\n const sizeClasses = {\n sm: \"w-8 h-8\",\n md: \"w-12 h-12\",\n lg: \"w-16 h-16\",\n }\n\n const statusColors = isDark\n ? {\n active: \"rgba(163, 163, 163, 0.7)\",\n inactive: \"rgba(115, 115, 115, 0.4)\",\n processing: \"rgba(163, 163, 163, 0.5)\",\n error: \"rgba(120, 113, 108, 0.6)\",\n }\n : {\n active: \"rgba(64, 64, 64, 0.8)\",\n inactive: \"rgba(100, 100, 100, 0.5)\",\n processing: \"rgba(64, 64, 64, 0.6)\",\n error: \"rgba(180, 83, 83, 0.7)\",\n }\n\n const color = glowColor || statusColors[status]\n\n return (\n \n {/* Pulse animation for processing state */}\n {status === \"processing\" && (\n \n )}\n\n {/* Active glow */}\n {status === \"active\" && (\n \n )}\n\n {/* Error pulse */}\n {status === \"error\" && (\n \n )}\n\n
\n {children}\n
\n \n )\n}\n\n// Animated trace line component for custom layouts\ninterface CircuitTraceProps {\n path: string\n animated?: boolean\n color?: string\n pulseColor?: string\n width?: number\n pulseSpeed?: number\n}\n\nfunction CircuitTrace({\n path,\n animated = true,\n color,\n pulseColor,\n width = 2,\n pulseSpeed = 2,\n}: CircuitTraceProps) {\n const [isDark, setIsDark] = React.useState(true)\n\n React.useEffect(() => {\n const checkTheme = () => {\n const isDarkMode = document.documentElement.classList.contains(\"dark\") ||\n document.body.classList.contains(\"dark\")\n setIsDark(isDarkMode)\n }\n\n checkTheme()\n\n const observer = new MutationObserver(checkTheme)\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] })\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] })\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\")\n mediaQuery.addEventListener(\"change\", checkTheme)\n\n return () => {\n observer.disconnect()\n mediaQuery.removeEventListener(\"change\", checkTheme)\n }\n }, [])\n\n const computedColor = color || (isDark ? \"rgba(163, 163, 163, 0.25)\" : \"rgba(64, 64, 64, 0.35)\")\n const computedPulseColor = pulseColor || (isDark ? \"rgba(163, 163, 163, 0.6)\" : \"rgba(64, 64, 64, 0.7)\")\n const pathLength = 500\n\n return (\n \n \n \n \n \n \n \n \n \n \n\n {/* Base trace */}\n \n\n {/* Animated pulse */}\n {animated && (\n \n )}\n \n )\n}\n\nexport {\n CircuitBoard,\n CircuitPattern,\n CircuitNode,\n CircuitTrace,\n type CircuitNode as CircuitNodeType,\n type CircuitConnection,\n type CircuitBoardProps,\n}\n", "type": "registry:ui" } ] }