{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "n8n-workflow-block-shadcnui", "type": "registry:block", "title": "N8N Workflow Block", "description": "Visual workflow automation builder with animated nodes, connections, and real-time execution monitoring", "registryDependencies": [ "button" ], "dependencies": [ "framer-motion", "react" ], "files": [ { "path": "@uitripled/react-shadcn/src/components/sections/n8n-workflow-block.tsx", "content": "\"use client\";\n\nimport { motion, type PanInfo } from \"framer-motion\";\nimport type React from \"react\";\nimport { useRef, useState } from \"react\";\nimport { flushSync } from \"react-dom\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport {\n ArrowRight,\n Database,\n Mail,\n Plus,\n Settings,\n Webhook,\n Zap,\n} from \"lucide-react\";\n\n// Interfaces\ninterface WorkflowNode {\n id: string;\n type: \"trigger\" | \"action\" | \"condition\";\n title: string;\n description: string;\n icon: React.ComponentType<{ className?: string }>;\n color: string;\n position: { x: number; y: number };\n}\n\ninterface WorkflowConnection {\n from: string;\n to: string;\n}\n\n// Constants\nconst NODE_WIDTH = 200;\nconst NODE_HEIGHT = 100;\n\nconst nodeTemplates: Omit[] = [\n {\n type: \"trigger\",\n title: \"Webhook\",\n description: \"Receive data from external service\",\n icon: Webhook,\n color: \"emerald\",\n },\n {\n type: \"action\",\n title: \"Database Query\",\n description: \"Fetch user records\",\n icon: Database,\n color: \"blue\",\n },\n {\n type: \"condition\",\n title: \"Condition\",\n description: \"Check user status\",\n icon: Settings,\n color: \"amber\",\n },\n {\n type: \"action\",\n title: \"Send Email\",\n description: \"Notify user\",\n icon: Mail,\n color: \"purple\",\n },\n {\n type: \"action\",\n title: \"Log Event\",\n description: \"Record activity\",\n icon: Zap,\n color: \"indigo\",\n },\n];\n\nconst initialNodes: WorkflowNode[] = [\n {\n id: \"node-1\",\n type: \"trigger\",\n title: \"Webhook\",\n description: \"Receive data from external service\",\n icon: Webhook,\n color: \"emerald\",\n position: { x: 50, y: 100 },\n },\n {\n id: \"node-2\",\n type: \"action\",\n title: \"Database Query\",\n description: \"Fetch user records\",\n icon: Database,\n color: \"blue\",\n position: { x: 300, y: 100 },\n },\n {\n id: \"node-3\",\n type: \"condition\",\n title: \"Condition\",\n description: \"Check user status\",\n icon: Settings,\n color: \"amber\",\n position: { x: 550, y: 100 },\n },\n];\n\nconst initialConnections: WorkflowConnection[] = [\n { from: \"node-1\", to: \"node-2\" },\n { from: \"node-2\", to: \"node-3\" },\n];\n\nconst colorClasses: Record = {\n emerald: \"border-emerald-400/40 bg-emerald-400/10 text-emerald-400\",\n blue: \"border-blue-400/40 bg-blue-400/10 text-blue-400\",\n amber: \"border-amber-400/40 bg-amber-400/10 text-amber-400\",\n purple: \"border-purple-400/40 bg-purple-400/10 text-purple-400\",\n indigo: \"border-indigo-400/40 bg-indigo-400/10 text-indigo-400\",\n};\n\n// Connection Line Component\nfunction WorkflowConnectionLine({\n from,\n to,\n nodes,\n}: {\n from: string;\n to: string;\n nodes: WorkflowNode[];\n}) {\n const fromNode = nodes.find((n) => n.id === from);\n const toNode = nodes.find((n) => n.id === to);\n if (!fromNode || !toNode) return null;\n\n const startX = fromNode.position.x + NODE_WIDTH;\n const startY = fromNode.position.y + NODE_HEIGHT / 2;\n const endX = toNode.position.x;\n const endY = toNode.position.y + NODE_HEIGHT / 2;\n\n const cp1X = startX + (endX - startX) * 0.5;\n const cp2X = endX - (endX - startX) * 0.5;\n\n const path = `M${startX},${startY} C${cp1X},${startY} ${cp2X},${endY} ${endX},${endY}`;\n\n return (\n \n );\n}\n\n// Main Component\nexport function N8nWorkflowBlock() {\n const [nodes, setNodes] = useState(initialNodes);\n const [connections, setConnections] =\n useState(initialConnections);\n const canvasRef = useRef(null);\n const dragStartPosition = useRef<{ x: number; y: number } | null>(null);\n const [draggingNodeId, setDraggingNodeId] = useState(null);\n const [contentSize, setContentSize] = useState(() => {\n const maxX = Math.max(\n ...initialNodes.map((n) => n.position.x + NODE_WIDTH)\n );\n const maxY = Math.max(\n ...initialNodes.map((n) => n.position.y + NODE_HEIGHT)\n );\n return { width: maxX + 50, height: maxY + 50 };\n });\n\n // Drag Handlers\n const handleDragStart = (nodeId: string) => {\n setDraggingNodeId(nodeId);\n const node = nodes.find((n) => n.id === nodeId);\n if (node) {\n dragStartPosition.current = { x: node.position.x, y: node.position.y };\n }\n };\n\n const handleDrag = (nodeId: string, { offset }: PanInfo) => {\n if (draggingNodeId !== nodeId || !dragStartPosition.current) return;\n\n const newX = dragStartPosition.current.x + offset.x;\n const newY = dragStartPosition.current.y + offset.y;\n\n const constrainedX = Math.max(0, newX);\n const constrainedY = Math.max(0, newY);\n\n flushSync(() => {\n setNodes((prev) =>\n prev.map((node) =>\n node.id === nodeId\n ? { ...node, position: { x: constrainedX, y: constrainedY } }\n : node\n )\n );\n });\n\n setContentSize((prev) => ({\n width: Math.max(prev.width, constrainedX + NODE_WIDTH + 50),\n height: Math.max(prev.height, constrainedY + NODE_HEIGHT + 50),\n }));\n };\n\n const handleDragEnd = () => {\n setDraggingNodeId(null);\n dragStartPosition.current = null;\n };\n\n // Add Node Handler\n const addNode = () => {\n const template =\n nodeTemplates[Math.floor(Math.random() * nodeTemplates.length)];\n const lastNode = nodes[nodes.length - 1];\n const newPosition = lastNode\n ? { x: lastNode.position.x + 250, y: lastNode.position.y }\n : { x: 50, y: 100 };\n\n const newNode: WorkflowNode = {\n id: `node-${Date.now()}`,\n ...template,\n position: newPosition,\n };\n\n flushSync(() => {\n setNodes((prev) => [...prev, newNode]);\n if (lastNode) {\n setConnections((prev) => [\n ...prev,\n { from: lastNode.id, to: newNode.id },\n ]);\n }\n });\n\n setContentSize((prev) => ({\n width: Math.max(prev.width, newPosition.x + NODE_WIDTH + 50),\n height: Math.max(prev.height, newPosition.y + NODE_HEIGHT + 50),\n }));\n\n // Scroll to new node\n const canvas = canvasRef.current;\n if (canvas) {\n canvas.scrollTo({\n left: newPosition.x + NODE_WIDTH - canvas.clientWidth + 100,\n behavior: \"smooth\",\n });\n }\n };\n\n return (\n
\n {/* Header */}\n
\n
\n \n Active\n \n \n Workflow Builder\n \n
\n \n \n Add Node\n \n
\n\n {/* Canvas */}\n \n {/* Content Wrapper */}\n \n {/* SVG Connections */}\n \n {connections.map((c) => (\n \n ))}\n \n\n {/* Nodes */}\n {nodes.map((node) => {\n const Icon = node.icon;\n const isDragging = draggingNodeId === node.id;\n\n return (\n handleDragStart(node.id)}\n onDrag={(_, info) => handleDrag(node.id, info)}\n onDragEnd={handleDragEnd}\n style={{\n x: node.position.x,\n y: node.position.y,\n width: NODE_WIDTH,\n transformOrigin: \"0 0\",\n }}\n className=\"absolute cursor-grab\"\n initial={{ scale: 0.8, opacity: 0 }}\n animate={{ scale: 1, opacity: 1 }}\n transition={{ duration: 0.2 }}\n whileHover={{ scale: 1.02 }}\n whileDrag={{ scale: 1.05, zIndex: 50, cursor: \"grabbing\" }}\n aria-grabbed={isDragging}\n >\n \n
\n\n
\n
\n \n \n
\n
\n \n {node.type}\n \n

\n {node.title}\n

\n
\n
\n

\n {node.description}\n

\n
\n \n \n Connected\n \n
\n
\n \n \n );\n })}\n
\n \n\n {/* Footer Stats */}\n \n
\n
\n \n \n {nodes.length} {nodes.length === 1 ? \"Node\" : \"Nodes\"}\n \n
\n
\n \n \n {connections.length}{\" \"}\n {connections.length === 1 ? \"Connection\" : \"Connections\"}\n \n
\n
\n

\n Drag nodes to reposition\n

\n \n \n );\n}\n", "type": "registry:block", "target": "components/uitripled/n8n-workflow-block-shadcnui.tsx" } ] }