{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "data-flow-pipes", "title": "Data Flow Pipes", "description": "Glowing data packets travel along SVG bezier pipes between system nodes, leaving fading trails.", "dependencies": [ "remotion" ], "files": [ { "path": "registry/remocn/data-flow-pipes/index.tsx", "content": "\"use client\";\n\nimport { interpolate, useCurrentFrame } from \"remotion\";\n\nexport interface DataFlowNode {\n id: string;\n x: number;\n y: number;\n label?: string;\n}\n\nexport interface DataFlowEdge {\n from: string;\n to: string;\n /** Frame when the pulse on this edge starts. */\n startFrame?: number;\n}\n\nexport interface DataFlowPipesProps {\n nodes?: DataFlowNode[];\n edges?: DataFlowEdge[];\n pipeColor?: string;\n pulseColor?: string;\n pulseLength?: number;\n pulseDuration?: number;\n background?: string;\n nodeColor?: string;\n textColor?: string;\n speed?: number;\n className?: string;\n}\n\nconst FONT_FAMILY =\n \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\";\n\nconst DEFAULT_NODES: DataFlowNode[] = [\n { id: \"client\", x: 140, y: 360, label: \"Client\" },\n { id: \"api\", x: 460, y: 200, label: \"API\" },\n { id: \"queue\", x: 460, y: 520, label: \"Queue\" },\n { id: \"db\", x: 820, y: 360, label: \"Database\" },\n { id: \"cdn\", x: 1140, y: 200, label: \"CDN\" },\n { id: \"log\", x: 1140, y: 520, label: \"Logs\" },\n];\n\nconst DEFAULT_EDGES: DataFlowEdge[] = [\n { from: \"client\", to: \"api\", startFrame: 0 },\n { from: \"client\", to: \"queue\", startFrame: 12 },\n { from: \"api\", to: \"db\", startFrame: 24 },\n { from: \"queue\", to: \"db\", startFrame: 36 },\n { from: \"db\", to: \"cdn\", startFrame: 48 },\n { from: \"db\", to: \"log\", startFrame: 56 },\n];\n\n/**\n * Build a smooth cubic bezier path string between two points. The control\n * points are pulled toward the midpoint and offset perpendicular for natural\n * routing — straight lines look like circuit traces, curves look like data.\n */\nfunction bezierPath(a: { x: number; y: number }, b: { x: number; y: number }) {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n // Stronger horizontal handles → routes look like API/DB diagrams.\n const handle = Math.max(60, Math.abs(dx) * 0.5);\n const c1x = a.x + handle;\n const c1y = a.y;\n const c2x = b.x - handle;\n const c2y = b.y;\n return `M ${a.x} ${a.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${b.x} ${b.y}`;\n}\n\n/**\n * Approximate arc length of a cubic bezier by sampling. Gives a stable number\n * to drive `strokeDashoffset` against — purely numeric, no DOM `getTotalLength`.\n */\nfunction bezierLength(\n a: { x: number; y: number },\n b: { x: number; y: number },\n) {\n const handle = Math.max(60, Math.abs(b.x - a.x) * 0.5);\n const c1 = { x: a.x + handle, y: a.y };\n const c2 = { x: b.x - handle, y: b.y };\n let len = 0;\n let prev = a;\n const steps = 32;\n for (let i = 1; i <= steps; i++) {\n const t = i / steps;\n const u = 1 - t;\n const p = {\n x:\n u * u * u * a.x +\n 3 * u * u * t * c1.x +\n 3 * u * t * t * c2.x +\n t * t * t * b.x,\n y:\n u * u * u * a.y +\n 3 * u * u * t * c1.y +\n 3 * u * t * t * c2.y +\n t * t * t * b.y,\n };\n len += Math.hypot(p.x - prev.x, p.y - prev.y);\n prev = p;\n }\n return len;\n}\n\nexport function DataFlowPipes({\n nodes = DEFAULT_NODES,\n edges = DEFAULT_EDGES,\n pipeColor = \"#1f1f23\",\n pulseColor = \"#22d3ee\",\n pulseLength = 60,\n pulseDuration = 36,\n background = \"#050505\",\n nodeColor = \"#0a0a0a\",\n textColor = \"#fafafa\",\n speed = 1,\n className,\n}: DataFlowPipesProps) {\n const frame = useCurrentFrame() * speed;\n const nodeMap = new Map(nodes.map((n) => [n.id, n]));\n\n return (\n \n \n {/* Static pipes */}\n {edges.map((edge, i) => {\n const a = nodeMap.get(edge.from);\n const b = nodeMap.get(edge.to);\n if (!a || !b) return null;\n const path = bezierPath(a, b);\n return (\n \n );\n })}\n\n {/* Pulses with trailing ghosts */}\n {edges.map((edge, i) => {\n const a = nodeMap.get(edge.from);\n const b = nodeMap.get(edge.to);\n if (!a || !b) return null;\n const path = bezierPath(a, b);\n const len = bezierLength(a, b);\n const startFrame = edge.startFrame ?? 0;\n const localFrame = frame - startFrame;\n\n // Pulse offset: starts above the path (out of view), travels down\n // until the dash sits past the end. We use a wide gap (9999) so the\n // dash pattern shows exactly one bright segment.\n const offset = interpolate(\n localFrame,\n [0, pulseDuration],\n [len + pulseLength, -pulseLength],\n { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n );\n\n // Hide pulse before its start window.\n if (localFrame < 0 || localFrame > pulseDuration + 6) {\n return null;\n }\n\n return (\n \n {/* Trail copies — fading and offset back along the path. */}\n {[0.15, 0.3, 0.55].map((alpha, idx) => (\n \n ))}\n {/* Bright head */}\n \n \n );\n })}\n \n\n {/* Nodes */}\n {nodes.map((node) => (\n \n {node.label ?? node.id}\n \n ))}\n \n );\n}\n", "type": "registry:component", "target": "components/remocn/data-flow-pipes.tsx" } ], "type": "registry:component" }