{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"canvas","type":"registry:ui","title":"Canvas","description":"Engine-pluggable pan\/zoom surface; DOM renderer built in.","package":"fancy-3d","dependencies":["lucide-react"],"registryDependencies":[],"files":[{"path":"components\/fancy\/canvas\/Canvas.context.ts","content":"import { createContext, useContext } from \"react\";\r\nimport type { CanvasContextValue } from \".\/Canvas.types\";\r\n\r\nexport const CanvasContext = createContext<CanvasContextValue | null>(null);\r\n\r\nexport function useCanvas(): CanvasContextValue {\r\n  const ctx = useContext(CanvasContext);\r\n  if (!ctx) throw new Error(\"useCanvas must be used within a Canvas component\");\r\n  return ctx;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/Canvas.context.ts"},{"path":"components\/fancy\/canvas\/Canvas.engine.ts","content":"import type { ViewportState } from \"..\/hooks\/use-pan-zoom\";\r\n\r\n\/**\r\n * The CanvasEngine interface lets `<Canvas>` host any 3D backend alongside\r\n * its 2D pan\/zoom surface.\r\n *\r\n * fancy-3d itself ships only the engine-agnostic core + the `dom` engine\r\n * (plain DOM \/ CSS3D). WebGL engines live in companion adapter packages:\r\n *\r\n *   - `@particle-academy\/fancy-3d-babylon` for BabylonJS\r\n *   - (sibling packages can ship three.js, WebGPU, WebXR, etc.)\r\n *\r\n * Implement this interface to plug in any future spatial runtime. Pass the\r\n * implementation directly to `<Canvas engine={yourEngine}>`.\r\n *\/\r\nexport interface CanvasEngine {\r\n  \/** Identifier surfaced via {@link EngineHandle.name} for adapters that\r\n   *  branch on engine type. *\/\r\n  name: string;\r\n\r\n  \/**\r\n   * Mount the engine onto a host element (the Canvas's outer container).\r\n   * Called once when the Canvas mounts. Return an {@link EngineHandle} that\r\n   * exposes the engine's scene root and lifecycle hooks.\r\n   *\/\r\n  mount(host: HTMLElement, viewport: ViewportState): EngineHandle;\r\n}\r\n\r\nexport interface EngineHandle {\r\n  \/** Same as {@link CanvasEngine.name}. *\/\r\n  name: string;\r\n  \/**\r\n   * Engine-native scene root. Type is `unknown` because the shape varies per\r\n   * engine \u2014 Babylon engines expose a `BABYLON.Scene`, three.js a `THREE.Scene`,\r\n   * the `dom` engine the host `HTMLElement` itself.\r\n   *\r\n   * Adapter components that target a specific engine should narrow with\r\n   * `if (engine?.name === \"babylon\") { const scene = engine.root as Scene; ... }`.\r\n   *\/\r\n  root: unknown;\r\n  \/**\r\n   * Sync engine state from Canvas pan\/zoom. For 2D engines this is the\r\n   * `ViewportState` directly; 3D engines may map it to camera transforms or\r\n   * (for MR) leave it as a no-op when 6DoF tracking owns viewport state.\r\n   *\/\r\n  updateViewport(viewport: ViewportState): void;\r\n  \/** Engine teardown on Canvas unmount. *\/\r\n  dispose(): void;\r\n}\r\n\r\n\/**\r\n * String shorthand for built-in engines. Only `\"dom\"` is currently built\r\n * into fancy-3d itself; pass a `CanvasEngine` object for any other backend\r\n * (e.g. `babylonEngine` from `@particle-academy\/fancy-3d-babylon\/engine`).\r\n *\/\r\nexport type CanvasEngineSpec = CanvasEngine | \"dom\";\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/Canvas.engine.ts"},{"path":"components\/fancy\/canvas\/Canvas.tsx","content":"import { useMemo, useRef, useEffect, useState, Children, type ReactElement } from \"react\";\r\nimport { cn } from \"..\/utils\/cn\";\r\nimport { useControllableState } from \"..\/hooks\/use-controllable-state\";\r\nimport { usePanZoom, type ViewportState } from \"..\/hooks\/use-pan-zoom\";\r\nimport { useNodeRegistry } from \"..\/hooks\/use-node-registry\";\r\nimport { CanvasContext } from \".\/Canvas.context\";\r\nimport { CanvasNode } from \".\/CanvasNode\";\r\nimport { CanvasEdge } from \".\/CanvasEdge\";\r\nimport { CanvasMinimap } from \".\/CanvasMinimap\";\r\nimport { CanvasControls } from \".\/CanvasControls\";\r\nimport { domEngine } from \".\/engines\/dom\";\r\nimport type { CanvasEngine, CanvasEngineSpec, EngineHandle } from \".\/Canvas.engine\";\r\nimport type { CanvasProps } from \".\/Canvas.types\";\r\n\r\n\/\/ fancy-3d ships only the engine-agnostic core + the DOM engine.\r\n\/\/ WebGL engines live in companion packages (e.g. @particle-academy\/fancy-3d-babylon)\r\n\/\/ \u2014 import the engine from there and pass the object directly via <Canvas engine={...}>.\r\nfunction resolveEngine(spec: CanvasEngineSpec | undefined): CanvasEngine {\r\n  if (!spec || spec === \"dom\") return domEngine;\r\n  return spec;\r\n}\r\n\r\nconst DEFAULT_VIEWPORT: ViewportState = { panX: 0, panY: 0, zoom: 1 };\r\n\r\nfunction CanvasRoot({\r\n  children,\r\n  viewport: controlledViewport,\r\n  defaultViewport = DEFAULT_VIEWPORT,\r\n  onViewportChange,\r\n  minZoom = 0.1,\r\n  maxZoom = 3,\r\n  pannable = true,\r\n  zoomable = true,\r\n  showGrid = false,\r\n  gridStyle = \"dots\",\r\n  gridSize = 20,\r\n  gridColor = \"rgb(161 161 170 \/ 0.3)\",\r\n  snapToGrid = false,\r\n  fitOnMount = false,\r\n  engine: engineSpec,\r\n  className,\r\n  style,\r\n}: CanvasProps) {\r\n  const containerRef = useRef<HTMLDivElement>(null);\r\n  const [viewport, setViewport] = useControllableState(controlledViewport, defaultViewport, onViewportChange);\r\n  const { registerNode, unregisterNode, nodeRects, version: registryVersion } = useNodeRegistry();\r\n  const [engineHandle, setEngineHandle] = useState<EngineHandle | null>(null);\r\n\r\n  \/\/ Mount the chosen engine once per spec change. Default \"dom\" engine is a\r\n  \/\/ no-op so this does not affect existing consumers.\r\n  useEffect(() => {\r\n    const host = containerRef.current;\r\n    if (!host) return;\r\n    const resolved = resolveEngine(engineSpec);\r\n    let handle: EngineHandle | null = null;\r\n    try {\r\n      handle = resolved.mount(host, viewport);\r\n      setEngineHandle(handle);\r\n    } catch (err) {\r\n      console.error(`[fancy-3d] Canvas engine \"${resolved.name}\" failed to mount:`, err);\r\n      setEngineHandle(null);\r\n    }\r\n    return () => {\r\n      try { handle?.dispose(); } catch (err) {\r\n        console.error(`[fancy-3d] Canvas engine \"${handle?.name}\" failed to dispose:`, err);\r\n      }\r\n      setEngineHandle(null);\r\n    };\r\n    \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\r\n  }, [engineSpec]);\r\n\r\n  \/\/ Push viewport changes to the active engine so it can mirror pan\/zoom.\r\n  useEffect(() => {\r\n    engineHandle?.updateViewport(viewport);\r\n  }, [engineHandle, viewport]);\r\n\r\n  const { containerProps } = usePanZoom({\r\n    viewport,\r\n    setViewport,\r\n    minZoom,\r\n    maxZoom,\r\n    pannable,\r\n    zoomable,\r\n    containerRef,\r\n  });\r\n\r\n  const ctx = useMemo(\r\n    () => ({ viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, containerRef, gridSize, snapToGrid, engine: engineHandle }),\r\n    [viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, gridSize, snapToGrid, engineHandle],\r\n  );\r\n\r\n  \/\/ Auto-fit all nodes into view on mount once nodes are registered\r\n  const hasFitted = useRef(false);\r\n  useEffect(() => {\r\n    if (!fitOnMount || hasFitted.current || nodeRects.size === 0) return;\r\n    const container = containerRef.current;\r\n    if (!container || container.clientWidth === 0) return;\r\n\r\n    hasFitted.current = true;\r\n\r\n    \/\/ Use rAF to ensure layout is settled before measuring\r\n    requestAnimationFrame(() => {\r\n      let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\r\n      nodeRects.forEach((r) => {\r\n        minX = Math.min(minX, r.x);\r\n        minY = Math.min(minY, r.y);\r\n        maxX = Math.max(maxX, r.x + r.width);\r\n        maxY = Math.max(maxY, r.y + r.height);\r\n      });\r\n\r\n      const padding = 40;\r\n      const contentW = maxX - minX + padding * 2;\r\n      const contentH = maxY - minY + padding * 2;\r\n      const cw = container.clientWidth;\r\n      const ch = container.clientHeight;\r\n      const zoom = Math.min(cw \/ contentW, ch \/ contentH, 1.5);\r\n      const panX = (cw - contentW * zoom) \/ 2 - minX * zoom + padding * zoom;\r\n      const panY = (ch - contentH * zoom) \/ 2 - minY * zoom + padding * zoom;\r\n\r\n      setViewport({ panX, panY, zoom });\r\n    });\r\n  }, [fitOnMount, nodeRects, registryVersion, setViewport]);\r\n\r\n  \/\/ Separate edge children from node\/other children\r\n  const edges: ReactElement[] = [];\r\n  const others: ReactElement[] = [];\r\n  const overlays: ReactElement[] = []; \/\/ minimap, controls\r\n\r\n  Children.forEach(children, (child) => {\r\n    const el = child as ReactElement;\r\n    if (!el || !el.type) return;\r\n    const elType = el.type as any;\r\n    if (elType === CanvasEdge || elType?._isCanvasEdge) {\r\n      edges.push(el);\r\n    } else if (elType === CanvasMinimap || elType === CanvasControls) {\r\n      overlays.push(el);\r\n    } else {\r\n      others.push(el);\r\n    }\r\n  });\r\n\r\n  return (\r\n    <CanvasContext.Provider value={ctx}>\r\n      <div\r\n        ref={containerRef}\r\n        data-react-fancy-canvas=\"\"\r\n        className={cn(\"relative overflow-hidden\", className)}\r\n        style={{ touchAction: \"none\", ...style }}\r\n        {...containerProps}\r\n      >\r\n        {\/* Background *\/}\r\n        <div\r\n          data-canvas-bg=\"\"\r\n          className=\"absolute inset-0\"\r\n          style={\r\n            showGrid && gridStyle !== \"none\"\r\n              ? gridStyle === \"lines\"\r\n                ? {\r\n                    backgroundImage: `linear-gradient(to right, ${gridColor} 1px, transparent 1px), linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`,\r\n                    backgroundSize: `${gridSize * viewport.zoom}px ${gridSize * viewport.zoom}px`,\r\n                    backgroundPosition: `${viewport.panX}px ${viewport.panY}px`,\r\n                  }\r\n                : {\r\n                    backgroundImage: `radial-gradient(circle, ${gridColor} 1px, transparent 1px)`,\r\n                    backgroundSize: `${gridSize * viewport.zoom}px ${gridSize * viewport.zoom}px`,\r\n                    backgroundPosition: `${viewport.panX}px ${viewport.panY}px`,\r\n                  }\r\n              : undefined\r\n          }\r\n        \/>\r\n\r\n        {\/* Transformed layer for nodes *\/}\r\n        <div\r\n          className=\"absolute origin-top-left\"\r\n          style={{\r\n            transform: `translate(${viewport.panX}px, ${viewport.panY}px) scale(${viewport.zoom})`,\r\n          }}\r\n        >\r\n          {others}\r\n        <\/div>\r\n\r\n        {\/* SVG layer for edges *\/}\r\n        <svg\r\n          className=\"pointer-events-none absolute inset-0 h-full w-full\"\r\n          style={{\r\n            transform: `translate(${viewport.panX}px, ${viewport.panY}px) scale(${viewport.zoom})`,\r\n            transformOrigin: \"0 0\",\r\n          }}\r\n        >\r\n          <defs>\r\n            {\/* Generic markers *\/}\r\n            <marker id=\"canvas-arrow\" viewBox=\"0 0 10 10\" refX=\"10\" refY=\"5\" markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\r\n              <path d=\"M0,0 L10,5 L0,10 Z\" fill=\"#71717a\" \/>\r\n            <\/marker>\r\n            <marker id=\"canvas-circle\" viewBox=\"0 0 10 10\" refX=\"5\" refY=\"5\" markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\r\n              <circle cx=\"5\" cy=\"5\" r=\"3.5\" fill=\"#71717a\" \/>\r\n            <\/marker>\r\n            <marker id=\"canvas-diamond\" viewBox=\"0 0 12 12\" refX=\"6\" refY=\"6\" markerWidth=\"10\" markerHeight=\"10\" orient=\"auto\">\r\n              <polygon points=\"6,0 12,6 6,12 0,6\" fill=\"none\" stroke=\"#71717a\" strokeWidth=\"1.5\" \/>\r\n            <\/marker>\r\n\r\n            {\/* ERD: \"one\" perpendicular bar at path endpoint *\/}\r\n            <marker id=\"canvas-one\" viewBox=\"0 0 2 16\" refX=\"1\" refY=\"8\" markerWidth=\"2\" markerHeight=\"14\" orient=\"auto\">\r\n              <line x1=\"1\" y1=\"0\" x2=\"1\" y2=\"16\" stroke=\"#71717a\" strokeWidth=\"2\" \/>\r\n            <\/marker>\r\n\r\n            {\/* ERD: crow's foot at path endpoint \u2014 fork fans backward along the line *\/}\r\n            {\/* orient=\"auto\" aligns x-axis with path direction at the endpoint.\r\n                At markerEnd the path goes left\u2192right into the entity.\r\n                refX=16 places the rightmost edge at the endpoint (entity side).\r\n                Fan lines go from right (x=16, entity) back-left into the line. *\/}\r\n            <marker id=\"canvas-crow-foot\" viewBox=\"0 0 16 16\" refX=\"16\" refY=\"8\" markerWidth=\"14\" markerHeight=\"14\" orient=\"auto\">\r\n              <line x1=\"16\" y1=\"8\" x2=\"0\" y2=\"0\" stroke=\"#71717a\" strokeWidth=\"2\" strokeLinecap=\"round\" \/>\r\n              <line x1=\"16\" y1=\"8\" x2=\"0\" y2=\"8\" stroke=\"#71717a\" strokeWidth=\"2\" strokeLinecap=\"round\" \/>\r\n              <line x1=\"16\" y1=\"8\" x2=\"0\" y2=\"16\" stroke=\"#71717a\" strokeWidth=\"2\" strokeLinecap=\"round\" \/>\r\n              <line x1=\"16\" y1=\"0\" x2=\"16\" y2=\"16\" stroke=\"#71717a\" strokeWidth=\"2\" strokeLinecap=\"round\" \/>\r\n            <\/marker>\r\n          <\/defs>\r\n          {edges}\r\n        <\/svg>\r\n\r\n        {\/* Overlays (minimap, controls) sit outside the transform *\/}\r\n        {overlays}\r\n      <\/div>\r\n    <\/CanvasContext.Provider>\r\n  );\r\n}\r\n\r\nexport const Canvas = Object.assign(CanvasRoot, {\r\n  Node: CanvasNode,\r\n  Edge: CanvasEdge,\r\n  Minimap: CanvasMinimap,\r\n  Controls: CanvasControls,\r\n});\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/Canvas.tsx"},{"path":"components\/fancy\/canvas\/Canvas.types.ts","content":"import type { ReactNode, CSSProperties } from \"react\";\r\nimport type { ViewportState } from \"..\/hooks\/use-pan-zoom\";\r\nimport type { NodeRect } from \"..\/hooks\/use-node-registry\";\r\nimport type { CanvasEngineSpec, EngineHandle } from \".\/Canvas.engine\";\r\n\r\nexport type { ViewportState } from \"..\/hooks\/use-pan-zoom\";\r\nexport type { CanvasEngine, CanvasEngineSpec, EngineHandle } from \".\/Canvas.engine\";\r\n\r\nexport type EdgeAnchor = \"top\" | \"bottom\" | \"left\" | \"right\" | \"center\" | \"auto\";\r\n\r\nexport interface CanvasContextValue {\r\n  viewport: ViewportState;\r\n  setViewport: (vp: ViewportState | ((prev: ViewportState) => ViewportState)) => void;\r\n  registerNode: (id: string, rect: NodeRect) => void;\r\n  unregisterNode: (id: string) => void;\r\n  nodeRects: Map<string, NodeRect>;\r\n  registryVersion: number;\r\n  containerRef: React.RefObject<HTMLDivElement | null>;\r\n  \/** Grid spacing in canvas-space pixels (unaffected by zoom). *\/\r\n  gridSize: number;\r\n  \/** When true, dragged nodes snap their top-left corner to the grid. *\/\r\n  snapToGrid: boolean;\r\n  \/** Active 3D engine, if any. `null` while the engine is mounting or when\r\n   *  the Canvas was created without an engine prop. *\/\r\n  engine: EngineHandle | null;\r\n}\r\n\r\nexport type GridStyle = \"dots\" | \"lines\" | \"none\";\r\n\r\nexport interface CanvasProps {\r\n  children: ReactNode;\r\n  viewport?: ViewportState;\r\n  defaultViewport?: ViewportState;\r\n  onViewportChange?: (viewport: ViewportState) => void;\r\n  minZoom?: number;\r\n  maxZoom?: number;\r\n  pannable?: boolean;\r\n  zoomable?: boolean;\r\n  \/** Grid spacing in canvas-space pixels. Defaults to 20. *\/\r\n  gridSize?: number;\r\n  \/** Show\/hide the canvas grid. Defaults to false. *\/\r\n  showGrid?: boolean;\r\n  \/** Grid pattern when shown \u2014 dots (default), lines, or none. Setting this\r\n   *  to \"none\" hides the grid even when `showGrid` is true. *\/\r\n  gridStyle?: GridStyle;\r\n  \/** Grid color (any CSS color). Defaults to a faint zinc. *\/\r\n  gridColor?: string;\r\n  \/** Snap dragged nodes to the grid. Defaults to false. *\/\r\n  snapToGrid?: boolean;\r\n  \/** Automatically fit all nodes into view on initial mount *\/\r\n  fitOnMount?: boolean;\r\n  \/**\r\n   * 3D engine to mount alongside the 2D canvas. Default `\"dom\"` (no extra\r\n   * engine \u2014 plain DOM\/CSS3D). For WebGL pass an engine object from a\r\n   * companion adapter package, e.g.\r\n   *\r\n   *   import { babylonEngine } from \"@particle-academy\/fancy-3d-babylon\/engine\";\r\n   *   <Canvas engine={babylonEngine} \/>\r\n   *\r\n   * Or implement `CanvasEngine` yourself for three.js, native canvas, WebXR,\r\n   * etc. The active engine is exposed via `useCanvas().engine`.\r\n   *\/\r\n  engine?: CanvasEngineSpec;\r\n  className?: string;\r\n  style?: CSSProperties;\r\n}\r\n\r\nexport interface CanvasNodeProps {\r\n  children: ReactNode;\r\n  id: string;\r\n  x: number;\r\n  y: number;\r\n  \/** Allow drag-to-move *\/\r\n  draggable?: boolean;\r\n  \/** Called when the node is dragged to a new position *\/\r\n  onPositionChange?: (x: number, y: number) => void;\r\n  className?: string;\r\n  style?: CSSProperties;\r\n}\r\n\r\nexport interface CanvasEdgeProps {\r\n  from: string;\r\n  to: string;\r\n  fromAnchor?: EdgeAnchor;\r\n  toAnchor?: EdgeAnchor;\r\n  curve?: \"bezier\" | \"step\" | \"straight\";\r\n  color?: string;\r\n  strokeWidth?: number;\r\n  dashed?: boolean;\r\n  animated?: boolean;\r\n  label?: ReactNode;\r\n  className?: string;\r\n  markerStart?: string;\r\n  markerEnd?: string;\r\n}\r\n\r\nexport interface CanvasMinimapProps {\r\n  width?: number;\r\n  height?: number;\r\n  className?: string;\r\n}\r\n\r\nexport interface CanvasControlsProps {\r\n  className?: string;\r\n  showZoomIn?: boolean;\r\n  showZoomOut?: boolean;\r\n  showReset?: boolean;\r\n  showFitAll?: boolean;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/Canvas.types.ts"},{"path":"components\/fancy\/canvas\/CanvasControls.tsx","content":"import { ZoomIn, ZoomOut, Maximize, RotateCcw } from \"lucide-react\";\r\nimport { cn } from \"..\/utils\/cn\";\r\nimport { useCanvas } from \".\/Canvas.context\";\r\nimport type { CanvasControlsProps } from \".\/Canvas.types\";\r\n\r\nexport function CanvasControls({\r\n  className,\r\n  showZoomIn = true,\r\n  showZoomOut = true,\r\n  showReset = true,\r\n  showFitAll = true,\r\n}: CanvasControlsProps) {\r\n  const { setViewport, nodeRects, containerRef } = useCanvas();\r\n\r\n  const zoomIn = () => setViewport((v) => ({ ...v, zoom: Math.min(3, v.zoom * 1.25) }));\r\n  const zoomOut = () => setViewport((v) => ({ ...v, zoom: Math.max(0.1, v.zoom \/ 1.25) }));\r\n  const reset = () => setViewport({ panX: 0, panY: 0, zoom: 1 });\r\n\r\n  const fitAll = () => {\r\n    const container = containerRef.current;\r\n    if (!container || nodeRects.size === 0) return reset();\r\n\r\n    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\r\n    nodeRects.forEach((r) => {\r\n      minX = Math.min(minX, r.x);\r\n      minY = Math.min(minY, r.y);\r\n      maxX = Math.max(maxX, r.x + r.width);\r\n      maxY = Math.max(maxY, r.y + r.height);\r\n    });\r\n\r\n    const padding = 40;\r\n    const contentW = maxX - minX + padding * 2;\r\n    const contentH = maxY - minY + padding * 2;\r\n    const cw = container.clientWidth;\r\n    const ch = container.clientHeight;\r\n    const zoom = Math.min(cw \/ contentW, ch \/ contentH, 1.5);\r\n    const panX = (cw - contentW * zoom) \/ 2 - minX * zoom + padding * zoom;\r\n    const panY = (ch - contentH * zoom) \/ 2 - minY * zoom + padding * zoom;\r\n\r\n    setViewport({ panX, panY, zoom });\r\n  };\r\n\r\n  const btnClass =\r\n    \"flex h-8 w-8 items-center justify-center rounded-md text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-300 transition-colors\";\r\n\r\n  return (\r\n    <div\r\n      data-react-fancy-canvas-controls=\"\"\r\n      className={cn(\r\n        \"absolute bottom-3 left-3 flex gap-1 rounded-lg border border-zinc-200 bg-white\/90 p-1 shadow-sm dark:border-zinc-700 dark:bg-zinc-900\/90\",\r\n        className,\r\n      )}\r\n    >\r\n      {showZoomIn && (\r\n        <button type=\"button\" onClick={zoomIn} className={btnClass} aria-label=\"Zoom in\">\r\n          <ZoomIn size={16} \/>\r\n        <\/button>\r\n      )}\r\n      {showZoomOut && (\r\n        <button type=\"button\" onClick={zoomOut} className={btnClass} aria-label=\"Zoom out\">\r\n          <ZoomOut size={16} \/>\r\n        <\/button>\r\n      )}\r\n      {showReset && (\r\n        <button type=\"button\" onClick={reset} className={btnClass} aria-label=\"Reset view\">\r\n          <RotateCcw size={16} \/>\r\n        <\/button>\r\n      )}\r\n      {showFitAll && (\r\n        <button type=\"button\" onClick={fitAll} className={btnClass} aria-label=\"Fit all\">\r\n          <Maximize size={16} \/>\r\n        <\/button>\r\n      )}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nCanvasControls.displayName = \"CanvasControls\";\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/CanvasControls.tsx"},{"path":"components\/fancy\/canvas\/CanvasEdge.tsx","content":"import { useMemo } from \"react\";\r\nimport { cn } from \"..\/utils\/cn\";\r\nimport { useCanvas } from \".\/Canvas.context\";\r\nimport { getAnchorPoint, getEdgePath } from \".\/canvas.utils\";\r\nimport type { CanvasEdgeProps } from \".\/Canvas.types\";\r\n\r\nexport function CanvasEdge({\r\n  from,\r\n  to,\r\n  fromAnchor = \"auto\",\r\n  toAnchor = \"auto\",\r\n  curve = \"bezier\",\r\n  color = \"currentColor\",\r\n  strokeWidth = 2,\r\n  dashed = false,\r\n  animated = false,\r\n  label,\r\n  className,\r\n  markerStart,\r\n  markerEnd,\r\n}: CanvasEdgeProps) {\r\n  const { nodeRects, registryVersion } = useCanvas();\r\n\r\n  const path = useMemo(() => {\r\n    const fromRect = nodeRects.get(from);\r\n    const toRect = nodeRects.get(to);\r\n    if (!fromRect || !toRect) return null;\r\n\r\n    const fromPt = getAnchorPoint(fromRect, fromAnchor, toRect);\r\n    const toPt = getAnchorPoint(toRect, toAnchor, fromRect);\r\n    return {\r\n      d: getEdgePath(fromPt, toPt, curve),\r\n      midX: (fromPt.x + toPt.x) \/ 2,\r\n      midY: (fromPt.y + toPt.y) \/ 2,\r\n    };\r\n  }, [from, to, fromAnchor, toAnchor, curve, nodeRects, registryVersion]);\r\n\r\n  if (!path) return null;\r\n\r\n  return (\r\n    <g data-react-fancy-canvas-edge=\"\" className={cn(\"text-zinc-300 dark:text-zinc-600\", className)}>\r\n      <path\r\n        d={path.d}\r\n        fill=\"none\"\r\n        stroke={color}\r\n        strokeWidth={strokeWidth}\r\n        strokeDasharray={dashed ? \"6 4\" : undefined}\r\n        markerStart={markerStart ? `url(#${markerStart})` : undefined}\r\n        markerEnd={markerEnd ? `url(#${markerEnd})` : undefined}\r\n        className={animated ? \"animate-[dash_1s_linear_infinite]\" : \"\"}\r\n        style={animated ? { strokeDasharray: \"8 4\" } : undefined}\r\n      \/>\r\n      {label && (\r\n        <foreignObject x={path.midX - 40} y={path.midY - 12} width={80} height={24}>\r\n          <div className=\"flex items-center justify-center text-xs text-zinc-500\">{label}<\/div>\r\n        <\/foreignObject>\r\n      )}\r\n    <\/g>\r\n  );\r\n}\r\n\r\nCanvasEdge.displayName = \"CanvasEdge\";\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/CanvasEdge.tsx"},{"path":"components\/fancy\/canvas\/CanvasMinimap.tsx","content":"import { useMemo } from \"react\";\r\nimport { cn } from \"..\/utils\/cn\";\r\nimport { useCanvas } from \".\/Canvas.context\";\r\nimport type { CanvasMinimapProps } from \".\/Canvas.types\";\r\n\r\nexport function CanvasMinimap({ width = 150, height = 100, className }: CanvasMinimapProps) {\r\n  const { nodeRects, registryVersion, viewport } = useCanvas();\r\n\r\n  const bounds = useMemo(() => {\r\n    if (nodeRects.size === 0) return { minX: 0, minY: 0, maxX: 500, maxY: 300 };\r\n    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\r\n    nodeRects.forEach((r) => {\r\n      minX = Math.min(minX, r.x);\r\n      minY = Math.min(minY, r.y);\r\n      maxX = Math.max(maxX, r.x + r.width);\r\n      maxY = Math.max(maxY, r.y + r.height);\r\n    });\r\n    const padding = 50;\r\n    return { minX: minX - padding, minY: minY - padding, maxX: maxX + padding, maxY: maxY + padding };\r\n  }, [nodeRects, registryVersion]);\r\n\r\n  const scaleX = width \/ (bounds.maxX - bounds.minX || 1);\r\n  const scaleY = height \/ (bounds.maxY - bounds.minY || 1);\r\n  const scale = Math.min(scaleX, scaleY);\r\n\r\n  return (\r\n    <div\r\n      data-react-fancy-canvas-minimap=\"\"\r\n      className={cn(\r\n        \"absolute right-3 bottom-3 overflow-hidden rounded-lg border border-zinc-200 bg-white\/90 dark:border-zinc-700 dark:bg-zinc-900\/90\",\r\n        className,\r\n      )}\r\n      style={{ width, height }}\r\n    >\r\n      <svg width={width} height={height}>\r\n        {Array.from(nodeRects.entries()).map(([id, rect]) => (\r\n          <rect\r\n            key={id}\r\n            x={(rect.x - bounds.minX) * scale}\r\n            y={(rect.y - bounds.minY) * scale}\r\n            width={Math.max(rect.width * scale, 4)}\r\n            height={Math.max(rect.height * scale, 3)}\r\n            rx={1}\r\n            className=\"fill-blue-400\/60\"\r\n          \/>\r\n        ))}\r\n        {\/* Viewport indicator *\/}\r\n        <rect\r\n          x={(-viewport.panX \/ viewport.zoom - bounds.minX) * scale}\r\n          y={(-viewport.panY \/ viewport.zoom - bounds.minY) * scale}\r\n          width={(width \/ viewport.zoom \/ scale > 0 ? width \/ viewport.zoom : width) * scale \/ (bounds.maxX - bounds.minX || 1) * (bounds.maxX - bounds.minX)}\r\n          height={(height \/ viewport.zoom \/ scale > 0 ? height \/ viewport.zoom : height) * scale \/ (bounds.maxY - bounds.minY || 1) * (bounds.maxY - bounds.minY)}\r\n          fill=\"none\"\r\n          stroke=\"currentColor\"\r\n          strokeWidth={1}\r\n          className=\"text-blue-500\"\r\n        \/>\r\n      <\/svg>\r\n    <\/div>\r\n  );\r\n}\r\n\r\nCanvasMinimap.displayName = \"CanvasMinimap\";\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/CanvasMinimap.tsx"},{"path":"components\/fancy\/canvas\/CanvasNode.tsx","content":"import { useEffect, useRef, useCallback } from \"react\";\r\nimport { cn } from \"..\/utils\/cn\";\r\nimport { useCanvas } from \".\/Canvas.context\";\r\nimport type { CanvasNodeProps } from \".\/Canvas.types\";\r\n\r\nexport function CanvasNode({ children, id, x, y, draggable, onPositionChange, className, style }: CanvasNodeProps) {\r\n  const { registerNode, unregisterNode, viewport, gridSize, snapToGrid } = useCanvas();\r\n  const nodeRef = useRef<HTMLDivElement>(null);\r\n  const isDragging = useRef(false);\r\n  const dragStart = useRef({ mouseX: 0, mouseY: 0, nodeX: 0, nodeY: 0 });\r\n\r\n  useEffect(() => {\r\n    const el = nodeRef.current;\r\n    if (!el) return;\r\n\r\n    const updateRect = () => {\r\n      registerNode(id, { x, y, width: el.offsetWidth, height: el.offsetHeight });\r\n    };\r\n\r\n    updateRect();\r\n\r\n    const observer = new ResizeObserver(updateRect);\r\n    observer.observe(el);\r\n\r\n    return () => {\r\n      observer.disconnect();\r\n      unregisterNode(id);\r\n    };\r\n  }, [id, x, y, registerNode, unregisterNode]);\r\n\r\n  const handlePointerDown = useCallback(\r\n    (e: React.PointerEvent) => {\r\n      if (!draggable || e.button !== 0) return;\r\n      e.stopPropagation();\r\n      isDragging.current = true;\r\n      dragStart.current = { mouseX: e.clientX, mouseY: e.clientY, nodeX: x, nodeY: y };\r\n      (e.target as HTMLElement).setPointerCapture(e.pointerId);\r\n    },\r\n    [draggable, x, y],\r\n  );\r\n\r\n  const handlePointerMove = useCallback(\r\n    (e: React.PointerEvent) => {\r\n      if (!isDragging.current) return;\r\n      const dx = (e.clientX - dragStart.current.mouseX) \/ viewport.zoom;\r\n      const dy = (e.clientY - dragStart.current.mouseY) \/ viewport.zoom;\r\n      let nx = dragStart.current.nodeX + dx;\r\n      let ny = dragStart.current.nodeY + dy;\r\n      if (snapToGrid && gridSize > 0) {\r\n        nx = Math.round(nx \/ gridSize) * gridSize;\r\n        ny = Math.round(ny \/ gridSize) * gridSize;\r\n      }\r\n      onPositionChange?.(nx, ny);\r\n    },\r\n    [viewport.zoom, onPositionChange, snapToGrid, gridSize],\r\n  );\r\n\r\n  const handlePointerUp = useCallback(() => {\r\n    isDragging.current = false;\r\n  }, []);\r\n\r\n  return (\r\n    <div\r\n      ref={nodeRef}\r\n      data-react-fancy-canvas-node=\"\"\r\n      data-node-id={id}\r\n      className={cn(\"absolute\", draggable && \"cursor-grab active:cursor-grabbing\", className)}\r\n      style={{ left: x, top: y, ...style }}\r\n      onPointerDown={handlePointerDown}\r\n      onPointerMove={handlePointerMove}\r\n      onPointerUp={handlePointerUp}\r\n    >\r\n      {children}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nCanvasNode.displayName = \"CanvasNode\";\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/CanvasNode.tsx"},{"path":"components\/fancy\/canvas\/canvas.utils.ts","content":"import type { NodeRect } from \"..\/hooks\/use-node-registry\";\r\nimport type { EdgeAnchor } from \".\/Canvas.types\";\r\n\r\nexport interface Point {\r\n  x: number;\r\n  y: number;\r\n}\r\n\r\nexport function getAnchorPoint(rect: NodeRect, anchor: EdgeAnchor, otherRect?: NodeRect): Point {\r\n  const cx = rect.x + rect.width \/ 2;\r\n  const cy = rect.y + rect.height \/ 2;\r\n\r\n  if (anchor === \"auto\" && otherRect) {\r\n    const ocx = otherRect.x + otherRect.width \/ 2;\r\n    const ocy = otherRect.y + otherRect.height \/ 2;\r\n    const dx = ocx - cx;\r\n    const dy = ocy - cy;\r\n\r\n    if (Math.abs(dx) > Math.abs(dy)) {\r\n      return dx > 0\r\n        ? { x: rect.x + rect.width, y: cy }\r\n        : { x: rect.x, y: cy };\r\n    }\r\n    return dy > 0\r\n      ? { x: cx, y: rect.y + rect.height }\r\n      : { x: cx, y: rect.y };\r\n  }\r\n\r\n  switch (anchor) {\r\n    case \"top\": return { x: cx, y: rect.y };\r\n    case \"bottom\": return { x: cx, y: rect.y + rect.height };\r\n    case \"left\": return { x: rect.x, y: cy };\r\n    case \"right\": return { x: rect.x + rect.width, y: cy };\r\n    case \"center\": return { x: cx, y: cy };\r\n    default: return { x: cx, y: cy };\r\n  }\r\n}\r\n\r\nexport function bezierPath(from: Point, to: Point): string {\r\n  const dx = Math.abs(to.x - from.x);\r\n  const dy = Math.abs(to.y - from.y);\r\n\r\n  \/\/ Determine if the connection is more horizontal or vertical\r\n  if (dx > dy) {\r\n    \/\/ Horizontal: control points extend horizontally\r\n    const offset = dx * 0.5;\r\n    const cp1x = from.x + (to.x > from.x ? offset : -offset);\r\n    const cp2x = to.x + (to.x > from.x ? -offset : offset);\r\n    return `M${from.x},${from.y} C${cp1x},${from.y} ${cp2x},${to.y} ${to.x},${to.y}`;\r\n  }\r\n  \/\/ Vertical: control points extend vertically\r\n  const offset = Math.max(dy * 0.5, 30);\r\n  const cp1y = from.y + (to.y > from.y ? offset : -offset);\r\n  const cp2y = to.y + (to.y > from.y ? -offset : offset);\r\n  return `M${from.x},${from.y} C${from.x},${cp1y} ${to.x},${cp2y} ${to.x},${to.y}`;\r\n}\r\n\r\nexport function stepPath(from: Point, to: Point): string {\r\n  const midX = (from.x + to.x) \/ 2;\r\n  return `M${from.x},${from.y} H${midX} V${to.y} H${to.x}`;\r\n}\r\n\r\nexport function straightPath(from: Point, to: Point): string {\r\n  return `M${from.x},${from.y} L${to.x},${to.y}`;\r\n}\r\n\r\nexport function getEdgePath(\r\n  from: Point,\r\n  to: Point,\r\n  curve: \"bezier\" | \"step\" | \"straight\" = \"bezier\",\r\n): string {\r\n  switch (curve) {\r\n    case \"bezier\": return bezierPath(from, to);\r\n    case \"step\": return stepPath(from, to);\r\n    case \"straight\": return straightPath(from, to);\r\n  }\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/canvas.utils.ts"},{"path":"components\/fancy\/canvas\/index.ts","content":"export { Canvas } from \".\/Canvas\";\r\nexport { useCanvas } from \".\/Canvas.context\";\r\nexport type {\r\n  CanvasProps,\r\n  CanvasNodeProps,\r\n  CanvasEdgeProps,\r\n  CanvasMinimapProps,\r\n  CanvasControlsProps,\r\n  CanvasContextValue,\r\n  ViewportState,\r\n  EdgeAnchor,\r\n  GridStyle,\r\n  CanvasEngine,\r\n  CanvasEngineSpec,\r\n  EngineHandle,\r\n} from \".\/Canvas.types\";\r\nexport { domEngine } from \".\/engines\/dom\";\r\n","type":"registry:ui","target":"components\/fancy\/canvas\/index.ts"}]}