{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "knowledge-graph",
  "type": "registry:ui",
  "title": "Knowledge Graph",
  "description": "An interactive D3.js-powered knowledge graph visualization with zoom, pan, drag, tooltips, and export functionality.",
  "dependencies": [
    "d3"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/knowledge-graph.tsx",
      "content": "\"use client\";\n\nimport {\n\tforwardRef,\n\tuseCallback,\n\tuseEffect,\n\tuseImperativeHandle,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport * as d3 from \"d3\";\nimport { cn } from \"@/lib/utils\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface GraphNode extends d3.SimulationNodeDatum {\n\t/** Unique identifier for the node */\n\tid: string;\n\t/** Display label for the node */\n\tlabel: string;\n\t/** Node type/category (used for coloring) */\n\ttype: string;\n\t/** Optional node size (default: 20) */\n\tsize?: number;\n\t/** Optional custom color */\n\tcolor?: string;\n\t/** Optional additional data */\n\tdata?: unknown;\n}\n\nexport interface GraphLink extends d3.SimulationLinkDatum<GraphNode> {\n\t/** Source node ID or node reference */\n\tsource: string | GraphNode;\n\t/** Target node ID or node reference */\n\ttarget: string | GraphNode;\n\t/** Optional relationship label */\n\tlabel?: string;\n\t/** Optional link strength (default: 1) */\n\tstrength?: number;\n}\n\nexport interface KnowledgeGraphProps {\n\t/** Array of nodes to display */\n\tnodes: GraphNode[];\n\t/** Array of links connecting nodes */\n\tlinks: GraphLink[];\n\t/** Callback when a node is clicked */\n\tonNodeClick?: (node: GraphNode) => void;\n\t/** Callback when a node is hovered */\n\tonNodeHover?: (node: GraphNode | null) => void;\n\t/** Show legend for node types */\n\tshowLegend?: boolean;\n\t/** Show link labels */\n\tshowLinkLabels?: boolean;\n\t/** Center node ID (will be positioned in center) */\n\tcenterNodeId?: string;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\nexport interface KnowledgeGraphHandle {\n\t/** Export the graph as SVG */\n\texportAsSVG: () => void;\n\t/** Export the graph as PNG */\n\texportAsPNG: () => void;\n\t/** Reset zoom to default */\n\tresetZoom: () => void;\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/** Generate a deterministic color from a string */\nconst stringToColor = (str: string): string => {\n\tlet hash = 0;\n\tfor (let i = 0; i < str.length; i++) {\n\t\tconst char = str.charCodeAt(i);\n\t\thash = (hash << 5) - hash + char;\n\t\thash = hash & hash;\n\t}\n\tconst hue = Math.abs(hash) % 360;\n\tconst saturation = 65 + (Math.abs(hash) % 20);\n\tconst lightness = 50 + (Math.abs(hash) % 15);\n\treturn `hsl(${hue}, ${saturation}%, ${lightness}%)`;\n};\n\n/** Format type string for display */\nconst formatTypeLabel = (type: string): string =>\n\ttype.replace(/_/g, \" \").replace(/\\b\\w/g, (l) => l.toUpperCase());\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KnowledgeGraph = forwardRef<\n\tKnowledgeGraphHandle,\n\tKnowledgeGraphProps\n>(\n\t(\n\t\t{\n\t\t\tnodes,\n\t\t\tlinks,\n\t\t\tonNodeClick,\n\t\t\tonNodeHover,\n\t\t\tshowLegend = true,\n\t\t\tshowLinkLabels = true,\n\t\t\tcenterNodeId,\n\t\t\tclassName = \"\",\n\t\t},\n\t\tref,\n\t) => {\n\t\tconst svgRef = useRef<SVGSVGElement>(null);\n\t\tconst containerRef = useRef<HTMLDivElement>(null);\n\t\tconst zoomRef = useRef<d3.ZoomBehavior<SVGSVGElement, unknown> | null>(\n\t\t\tnull,\n\t\t);\n\n\t\tconst [tooltip, setTooltip] = useState<{\n\t\t\tx: number;\n\t\t\ty: number;\n\t\t\tcontent: string;\n\t\t\tvisible: boolean;\n\t\t}>({\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\tcontent: \"\",\n\t\t\tvisible: false,\n\t\t});\n\n\t\tconst [legendItems, setLegendItems] = useState<\n\t\t\tArray<{ type: string; color: string }>\n\t\t>([]);\n\n\t\t// Export as SVG\n\t\tconst exportAsSVG = useCallback(() => {\n\t\t\tif (!svgRef.current) return;\n\n\t\t\tconst clonedSvg = svgRef.current.cloneNode(true) as SVGSVGElement;\n\t\t\tconst graphGroup = clonedSvg.querySelector(\"g\");\n\n\t\t\tif (graphGroup) {\n\t\t\t\tconst originalGroup = svgRef.current.querySelector(\"g\") as SVGGElement;\n\t\t\t\tif (originalGroup) {\n\t\t\t\t\tconst bbox = originalGroup.getBBox();\n\t\t\t\t\tconst padding = 50;\n\t\t\t\t\tclonedSvg.setAttribute(\n\t\t\t\t\t\t\"viewBox\",\n\t\t\t\t\t\t`${bbox.x - padding} ${bbox.y - padding} ${bbox.width + padding * 2} ${bbox.height + padding * 2}`,\n\t\t\t\t\t);\n\t\t\t\t\tclonedSvg.setAttribute(\"width\", `${bbox.width + padding * 2}`);\n\t\t\t\t\tclonedSvg.setAttribute(\"height\", `${bbox.height + padding * 2}`);\n\t\t\t\t\tgraphGroup.removeAttribute(\"transform\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst svgData = new XMLSerializer().serializeToString(clonedSvg);\n\t\t\tconst blob = new Blob([svgData], { type: \"image/svg+xml;charset=utf-8\" });\n\t\t\tconst url = URL.createObjectURL(blob);\n\t\t\tconst link = document.createElement(\"a\");\n\t\t\tlink.href = url;\n\t\t\tlink.download = \"knowledge-graph.svg\";\n\t\t\tlink.click();\n\t\t\tURL.revokeObjectURL(url);\n\t\t}, []);\n\n\t\t// Export as PNG\n\t\tconst exportAsPNG = useCallback(() => {\n\t\t\tif (!svgRef.current) return;\n\n\t\t\tconst clonedSvg = svgRef.current.cloneNode(true) as SVGSVGElement;\n\t\t\tconst graphGroup = clonedSvg.querySelector(\"g\");\n\t\t\tlet bbox = { x: 0, y: 0, width: 800, height: 600 };\n\n\t\t\tconst originalGroup = svgRef.current.querySelector(\"g\") as SVGGElement;\n\t\t\tif (originalGroup) {\n\t\t\t\tbbox = originalGroup.getBBox();\n\t\t\t}\n\n\t\t\tconst padding = 100;\n\t\t\tconst width = bbox.width + padding * 2;\n\t\t\tconst height = bbox.height + padding * 2;\n\n\t\t\tclonedSvg.setAttribute(\n\t\t\t\t\"viewBox\",\n\t\t\t\t`${bbox.x - padding} ${bbox.y - padding} ${width} ${height}`,\n\t\t\t);\n\t\t\tclonedSvg.setAttribute(\"width\", `${width}`);\n\t\t\tclonedSvg.setAttribute(\"height\", `${height}`);\n\t\t\tif (graphGroup) graphGroup.removeAttribute(\"transform\");\n\n\t\t\tconst canvas = document.createElement(\"canvas\");\n\t\t\tconst ctx = canvas.getContext(\"2d\");\n\t\t\tconst img = new window.Image();\n\t\t\tconst scale = 2;\n\n\t\t\tcanvas.width = width * scale;\n\t\t\tcanvas.height = height * scale;\n\t\t\tctx?.scale(scale, scale);\n\n\t\t\tif (ctx) {\n\t\t\t\tctx.fillStyle = \"#18181b\";\n\t\t\t\tctx.fillRect(0, 0, width, height);\n\t\t\t}\n\n\t\t\timg.onload = () => {\n\t\t\t\tctx?.drawImage(img, 0, 0, width, height);\n\t\t\t\tcanvas.toBlob((blob) => {\n\t\t\t\t\tif (blob) {\n\t\t\t\t\t\tconst url = URL.createObjectURL(blob);\n\t\t\t\t\t\tconst link = document.createElement(\"a\");\n\t\t\t\t\t\tlink.href = url;\n\t\t\t\t\t\tlink.download = \"knowledge-graph.png\";\n\t\t\t\t\t\tlink.click();\n\t\t\t\t\t\tURL.revokeObjectURL(url);\n\t\t\t\t\t}\n\t\t\t\t}, \"image/png\");\n\t\t\t};\n\n\t\t\tconst svgData = new XMLSerializer().serializeToString(clonedSvg);\n\t\t\tconst blob = new Blob([svgData], { type: \"image/svg+xml;charset=utf-8\" });\n\t\t\timg.src = URL.createObjectURL(blob);\n\t\t}, []);\n\n\t\t// Reset zoom\n\t\tconst resetZoom = useCallback(() => {\n\t\t\tif (svgRef.current && zoomRef.current) {\n\t\t\t\td3.select(svgRef.current)\n\t\t\t\t\t.transition()\n\t\t\t\t\t.duration(500)\n\t\t\t\t\t.call(zoomRef.current.transform, d3.zoomIdentity);\n\t\t\t}\n\t\t}, []);\n\n\t\tuseImperativeHandle(ref, () => ({\n\t\t\texportAsSVG,\n\t\t\texportAsPNG,\n\t\t\tresetZoom,\n\t\t}));\n\n\t\t// Main D3 effect\n\t\tuseEffect(() => {\n\t\t\tif (!svgRef.current || !containerRef.current || nodes.length === 0)\n\t\t\t\treturn;\n\n\t\t\tconst container = containerRef.current;\n\t\t\tconst svg = d3.select(svgRef.current);\n\n\t\t\t// Clear previous content\n\t\t\tsvg.selectAll(\"*\").remove();\n\n\t\t\tconst width = container.clientWidth;\n\t\t\tconst height = container.clientHeight;\n\n\t\t\t// Get unique node types for coloring\n\t\t\tconst nodeTypes = [...new Set(nodes.map((n) => n.type))].sort();\n\t\t\tconst colorMapping: Record<string, string> = {};\n\t\t\tfor (const type of nodeTypes) {\n\t\t\t\tcolorMapping[type] = stringToColor(type);\n\t\t\t}\n\n\t\t\tconst colorScale = (type: string, customColor?: string) =>\n\t\t\t\tcustomColor || colorMapping[type] || \"#6b7280\";\n\n\t\t\t// Set legend items\n\t\t\tsetLegendItems(\n\t\t\t\tnodeTypes.map((type) => ({\n\t\t\t\t\ttype: formatTypeLabel(type),\n\t\t\t\t\tcolor: colorMapping[type],\n\t\t\t\t})),\n\t\t\t);\n\n\t\t\t// Clone data to avoid mutation\n\t\t\tconst nodesCopy: GraphNode[] = nodes.map((n) => ({\n\t\t\t\t...n,\n\t\t\t\tsize: n.size || 20,\n\t\t\t}));\n\t\t\tconst linksCopy: GraphLink[] = links.map((l) => ({ ...l }));\n\n\t\t\t// Position center node if specified\n\t\t\tconst centerNode = centerNodeId\n\t\t\t\t? nodesCopy.find((n) => n.id === centerNodeId)\n\t\t\t\t: null;\n\t\t\tif (centerNode) {\n\t\t\t\tcenterNode.x = width / 2;\n\t\t\t\tcenterNode.y = height / 2;\n\t\t\t}\n\n\t\t\t// Create simulation\n\t\t\tconst simulation = d3\n\t\t\t\t.forceSimulation<GraphNode>(nodesCopy)\n\t\t\t\t.force(\n\t\t\t\t\t\"link\",\n\t\t\t\t\td3\n\t\t\t\t\t\t.forceLink<GraphNode, GraphLink>(linksCopy)\n\t\t\t\t\t\t.id((d: GraphNode) => d.id)\n\t\t\t\t\t\t.distance(150)\n\t\t\t\t\t\t.strength((d: GraphLink) => d.strength || 0.8),\n\t\t\t\t)\n\t\t\t\t.force(\"charge\", d3.forceManyBody().strength(-800))\n\t\t\t\t.force(\"center\", d3.forceCenter(width / 2, height / 2))\n\t\t\t\t.force(\n\t\t\t\t\t\"collision\",\n\t\t\t\t\td3\n\t\t\t\t\t\t.forceCollide<GraphNode>()\n\t\t\t\t\t\t.radius((d: GraphNode) => (d.size || 20) + 20),\n\t\t\t\t);\n\n\t\t\t// Create zoom behavior\n\t\t\tconst zoom = d3\n\t\t\t\t.zoom<SVGSVGElement, unknown>()\n\t\t\t\t.scaleExtent([0.1, 4])\n\t\t\t\t.on(\"zoom\", (event: d3.D3ZoomEvent<SVGSVGElement, unknown>) => {\n\t\t\t\t\tg.attr(\"transform\", event.transform.toString());\n\t\t\t\t});\n\n\t\t\tzoomRef.current = zoom;\n\t\t\tsvg.call(zoom);\n\n\t\t\t// Create container group\n\t\t\tconst g = svg.append(\"g\");\n\n\t\t\t// Create links\n\t\t\tconst link = g\n\t\t\t\t.selectAll<SVGLineElement, GraphLink>(\".link\")\n\t\t\t\t.data(linksCopy)\n\t\t\t\t.join(\"line\")\n\t\t\t\t.attr(\"class\", \"link\")\n\t\t\t\t.attr(\"stroke\", \"#6b7280\")\n\t\t\t\t.attr(\"stroke-opacity\", 0.5)\n\t\t\t\t.attr(\"stroke-width\", 2);\n\n\t\t\t// Create link labels\n\t\t\tconst linkLabel = showLinkLabels\n\t\t\t\t? g\n\t\t\t\t\t\t.selectAll<SVGTextElement, GraphLink>(\".link-label\")\n\t\t\t\t\t\t.data(linksCopy.filter((l) => l.label))\n\t\t\t\t\t\t.join(\"text\")\n\t\t\t\t\t\t.attr(\"class\", \"link-label\")\n\t\t\t\t\t\t.attr(\"text-anchor\", \"middle\")\n\t\t\t\t\t\t.attr(\"font-size\", \"10px\")\n\t\t\t\t\t\t.attr(\"fill\", \"#9ca3af\")\n\t\t\t\t\t\t.text((d: GraphLink) => d.label || \"\")\n\t\t\t\t: null;\n\n\t\t\t// Create node groups\n\t\t\tconst nodeGroup = g\n\t\t\t\t.selectAll<SVGGElement, GraphNode>(\".node-group\")\n\t\t\t\t.data(nodesCopy)\n\t\t\t\t.join(\"g\")\n\t\t\t\t.attr(\"class\", \"node-group\")\n\t\t\t\t.style(\"cursor\", \"pointer\")\n\t\t\t\t.call(\n\t\t\t\t\td3\n\t\t\t\t\t\t.drag<SVGGElement, GraphNode>()\n\t\t\t\t\t\t.on(\n\t\t\t\t\t\t\t\"start\",\n\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\tevent: d3.D3DragEvent<SVGGElement, GraphNode, GraphNode>,\n\t\t\t\t\t\t\t\td: GraphNode,\n\t\t\t\t\t\t\t) => {\n\t\t\t\t\t\t\t\tif (!event.active) simulation.alphaTarget(0.3).restart();\n\t\t\t\t\t\t\t\td.fx = d.x;\n\t\t\t\t\t\t\t\td.fy = d.y;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.on(\n\t\t\t\t\t\t\t\"drag\",\n\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\tevent: d3.D3DragEvent<SVGGElement, GraphNode, GraphNode>,\n\t\t\t\t\t\t\t\td: GraphNode,\n\t\t\t\t\t\t\t) => {\n\t\t\t\t\t\t\t\td.fx = event.x;\n\t\t\t\t\t\t\t\td.fy = event.y;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.on(\n\t\t\t\t\t\t\t\"end\",\n\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\tevent: d3.D3DragEvent<SVGGElement, GraphNode, GraphNode>,\n\t\t\t\t\t\t\t\td: GraphNode,\n\t\t\t\t\t\t\t) => {\n\t\t\t\t\t\t\t\tif (!event.active) simulation.alphaTarget(0);\n\t\t\t\t\t\t\t\td.fx = null;\n\t\t\t\t\t\t\t\td.fy = null;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t),\n\t\t\t\t);\n\n\t\t\t// Add circles\n\t\t\tnodeGroup\n\t\t\t\t.append(\"circle\")\n\t\t\t\t.attr(\"r\", (d: GraphNode) => d.size || 20)\n\t\t\t\t.attr(\"fill\", (d: GraphNode) => colorScale(d.type, d.color))\n\t\t\t\t.attr(\"stroke\", \"#27272a\")\n\t\t\t\t.attr(\"stroke-width\", 2);\n\n\t\t\t// Add labels\n\t\t\tnodeGroup\n\t\t\t\t.append(\"text\")\n\t\t\t\t.attr(\"class\", \"node-label\")\n\t\t\t\t.attr(\"text-anchor\", \"middle\")\n\t\t\t\t.attr(\"dy\", \".35em\")\n\t\t\t\t.attr(\"font-size\", \"11px\")\n\t\t\t\t.attr(\"font-weight\", \"500\")\n\t\t\t\t.attr(\"fill\", \"#ffffff\")\n\t\t\t\t.attr(\"pointer-events\", \"none\")\n\t\t\t\t.text((d: GraphNode) =>\n\t\t\t\t\td.label.length > 12 ? `${d.label.substring(0, 10)}...` : d.label,\n\t\t\t\t);\n\n\t\t\t// Add event handlers\n\t\t\tnodeGroup\n\t\t\t\t.on(\"click\", (_event: MouseEvent, d: GraphNode) => {\n\t\t\t\t\tonNodeClick?.(d);\n\t\t\t\t})\n\t\t\t\t.on(\"mouseover\", (event: MouseEvent, d: GraphNode) => {\n\t\t\t\t\tconst [x, y] = d3.pointer(event, container);\n\t\t\t\t\tsetTooltip({\n\t\t\t\t\t\tx: x + 10,\n\t\t\t\t\t\ty: y - 10,\n\t\t\t\t\t\tcontent: `${d.label} (${formatTypeLabel(d.type)})`,\n\t\t\t\t\t\tvisible: true,\n\t\t\t\t\t});\n\t\t\t\t\tonNodeHover?.(d);\n\t\t\t\t})\n\t\t\t\t.on(\"mouseout\", () => {\n\t\t\t\t\tsetTooltip((prev) => ({ ...prev, visible: false }));\n\t\t\t\t\tonNodeHover?.(null);\n\t\t\t\t});\n\n\t\t\t// Update positions on tick\n\t\t\tsimulation.on(\"tick\", () => {\n\t\t\t\tlink\n\t\t\t\t\t.attr(\"x1\", (d: GraphLink) => (d.source as GraphNode).x ?? 0)\n\t\t\t\t\t.attr(\"y1\", (d: GraphLink) => (d.source as GraphNode).y ?? 0)\n\t\t\t\t\t.attr(\"x2\", (d: GraphLink) => (d.target as GraphNode).x ?? 0)\n\t\t\t\t\t.attr(\"y2\", (d: GraphLink) => (d.target as GraphNode).y ?? 0);\n\n\t\t\t\tif (linkLabel) {\n\t\t\t\t\tlinkLabel\n\t\t\t\t\t\t.attr(\n\t\t\t\t\t\t\t\"x\",\n\t\t\t\t\t\t\t(d: GraphLink) =>\n\t\t\t\t\t\t\t\t(((d.source as GraphNode).x ?? 0) +\n\t\t\t\t\t\t\t\t\t((d.target as GraphNode).x ?? 0)) /\n\t\t\t\t\t\t\t\t2,\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.attr(\n\t\t\t\t\t\t\t\"y\",\n\t\t\t\t\t\t\t(d: GraphLink) =>\n\t\t\t\t\t\t\t\t(((d.source as GraphNode).y ?? 0) +\n\t\t\t\t\t\t\t\t\t((d.target as GraphNode).y ?? 0)) /\n\t\t\t\t\t\t\t\t2,\n\t\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tnodeGroup.attr(\n\t\t\t\t\t\"transform\",\n\t\t\t\t\t(d: GraphNode) => `translate(${d.x ?? 0},${d.y ?? 0})`,\n\t\t\t\t);\n\t\t\t});\n\n\t\t\t// Cleanup\n\t\t\treturn () => {\n\t\t\t\tsimulation.stop();\n\t\t\t};\n\t\t}, [nodes, links, onNodeClick, onNodeHover, showLinkLabels, centerNodeId]);\n\n\t\treturn (\n\t\t\t<div className={cn(\"relative h-full w-full min-h-[400px]\", className)}>\n\t\t\t\t<div ref={containerRef} className=\"h-full w-full\">\n\t\t\t\t\t<svg\n\t\t\t\t\t\tref={svgRef}\n\t\t\t\t\t\twidth=\"100%\"\n\t\t\t\t\t\theight=\"100%\"\n\t\t\t\t\t\tclassName=\"bg-zinc-50 dark:bg-zinc-900 rounded-xl\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\n\t\t\t\t{/* Tooltip */}\n\t\t\t\t{tooltip.visible && (\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"pointer-events-none absolute z-10 px-3 py-2 text-sm rounded-lg bg-zinc-800 text-zinc-100 shadow-lg border border-zinc-700\"\n\t\t\t\t\t\tstyle={{ left: tooltip.x, top: tooltip.y }}\n\t\t\t\t\t>\n\t\t\t\t\t\t{tooltip.content}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{/* Legend */}\n\t\t\t\t{showLegend && legendItems.length > 0 && (\n\t\t\t\t\t<div className=\"absolute top-4 right-4 z-10 p-3 rounded-lg bg-zinc-800/90 backdrop-blur-sm border border-zinc-700\">\n\t\t\t\t\t\t<div className=\"max-h-48 space-y-1.5 overflow-y-auto\">\n\t\t\t\t\t\t\t{legendItems.map((item) => (\n\t\t\t\t\t\t\t\t<div key={item.type} className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tclassName=\"h-3 w-3 rounded-full flex-shrink-0\"\n\t\t\t\t\t\t\t\t\t\tstyle={{ backgroundColor: item.color }}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<span className=\"text-xs text-zinc-300\">{item.type}</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t},\n);\n\nKnowledgeGraph.displayName = \"KnowledgeGraph\";\n\nexport default KnowledgeGraph;\n",
      "type": "registry:ui"
    }
  ]
}