{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora-ai-canvas",
  "title": "Aurora AI Canvas",
  "author": "jmagar <https://github.com/jmagar>",
  "description": "Aurora-native flow canvas with node edges and compact zoom actions.",
  "dependencies": [
    "lucide-react@^0.487.0"
  ],
  "registryDependencies": [
    "@aurora/aurora-tokens",
    "@aurora/aurora-ai-action"
  ],
  "files": [
    {
      "path": "registry/aurora/blocks/ai/elements/canvas.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Minus, Plus } from \"lucide-react\"\nimport { Action } from \"./action\"\nimport { cn } from \"@/lib/utils\"\n\n/* ------------------------------------------------------------------ *\n * Aurora · Canvas (AI element)\n *\n * A flow canvas with a header chip (status dot + title), a node-count\n * badge, a dotted-grid surface, absolutely-positioned nodes carrying\n * cyan input ports (left) + rose output ports (right), cyan bezier\n * edges that follow the nodes, and a zoom toolbar.\n *\n * Architecture (forwardRef, displayName, compound Node, HTMLAttributes\n * passthrough) is kept from the Aurora registry, while controls and typography\n * follow Aurora tokens and primitives.\n * ------------------------------------------------------------------ */\n\ntype CanvasStatus = \"running\" | \"idle\" | \"blocked\"\n\nexport interface CanvasEdge {\n  /** index of the source Node child */\n  from: number\n  /** index of the target Node child */\n  to: number\n}\n\nexport interface CanvasProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n  title?: React.ReactNode\n  status?: CanvasStatus\n  /** edges drawn between Node children, by child index */\n  edges?: CanvasEdge[]\n}\n\nexport interface NodeProps extends React.HTMLAttributes<HTMLDivElement> {\n  title: string\n  description?: string\n}\n\nconst statusGlow: Record<CanvasStatus, { dot: string; glow: string }> = {\n  running: {\n    dot: \"var(--aurora-accent-primary)\",\n    glow: \"0 0 0 3px color-mix(in srgb, var(--aurora-accent-primary) 22%, transparent), 0 0 12px color-mix(in srgb, var(--aurora-accent-primary) 60%, transparent)\",\n  },\n  idle: {\n    dot: \"var(--aurora-text-muted)\",\n    glow: \"0 0 0 3px color-mix(in srgb, var(--aurora-text-muted) 18%, transparent)\",\n  },\n  blocked: {\n    dot: \"var(--axon-orange)\",\n    glow: \"0 0 0 3px color-mix(in srgb, var(--axon-orange) 22%, transparent), 0 0 12px color-mix(in srgb, var(--axon-orange) 55%, transparent)\",\n  },\n}\n\n/* Geometry parsed from a Node child's inline style. Nodes are positioned with\n * absolute left/top + a fixed width; height is estimated from the node card\n * chrome so edges can anchor to the vertical port center. */\ntype NodeBox = { left: number; top: number; width: number; height: number }\n\nconst NODE_HEIGHT = 64\nconst PORT = 12\n\nfunction parseLen(value: string | number | undefined, fallback: number): number {\n  if (typeof value === \"number\") return value\n  if (typeof value === \"string\") {\n    const n = parseFloat(value)\n    if (!Number.isNaN(n)) return n\n  }\n  return fallback\n}\n\nfunction nodeBox(style: React.CSSProperties | undefined): NodeBox {\n  return {\n    left: parseLen(style?.left as string | number | undefined, 0),\n    top: parseLen(style?.top as string | number | undefined, 0),\n    width: parseLen(style?.width as string | number | undefined, 128),\n    height: parseLen(style?.height as string | number | undefined, NODE_HEIGHT),\n  }\n}\n\n/* Horizontal-tangent cubic bezier from a source right-port to a target\n * left-port so edges follow the nodes. */\nfunction edgePath(a: NodeBox, b: NodeBox): string {\n  const x1 = a.left + a.width\n  const y1 = a.top + a.height / 2\n  const x2 = b.left\n  const y2 = b.top + b.height / 2\n  const dx = Math.max(40, Math.abs(x2 - x1) * 0.6)\n  return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`\n}\n\nconst Node = ({ ref, title, description, className, style, ...props }: NodeProps & { ref?: React.Ref<HTMLDivElement> }) => (\n    <div\n      ref={ref}\n      className={cn(\"group/node\", className)}\n      style={{\n        position: \"absolute\",\n        boxSizing: \"border-box\",\n        padding: \"10px 14px\",\n        background: \"var(--aurora-surface-raised)\",\n        border: \"1px solid var(--aurora-border-strong)\",\n        borderRadius: \"var(--aurora-radius-1)\",\n        boxShadow: \"var(--aurora-shadow-medium), var(--aurora-highlight-medium)\",\n        cursor: \"grab\",\n        ...style,\n      }}\n      {...props}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 8,\n          fontFamily: \"var(--aurora-font-display)\",\n          fontWeight: 700,\n          fontSize: 15,\n          lineHeight: 1.2,\n          color: \"var(--aurora-text-primary)\",\n        }}\n      >\n        <span\n          aria-hidden\n          style={{\n            width: 8,\n            height: 8,\n            borderRadius: 9999,\n            flexShrink: 0,\n            background: \"var(--aurora-accent-primary)\",\n            boxShadow: \"0 0 8px color-mix(in srgb, var(--aurora-accent-primary) 60%, transparent)\",\n          }}\n        />\n        {title}\n      </div>\n      {description ? (\n        <div\n          style={{\n            marginTop: 4,\n            fontFamily: \"var(--aurora-font-sans)\",\n            fontSize: 13,\n            color: \"var(--aurora-text-muted)\",\n          }}\n        >\n          {description}\n        </div>\n      ) : null}\n\n      {/* input port (cyan, filled) — left edge */}\n      <span\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          left: -PORT / 2,\n          top: \"50%\",\n          transform: \"translateY(-50%)\",\n          width: PORT,\n          height: PORT,\n          borderRadius: 9999,\n          background: \"var(--aurora-accent-primary)\",\n          border: \"2px solid var(--aurora-page-bg)\",\n          boxShadow: \"0 0 8px color-mix(in srgb, var(--aurora-accent-primary) 55%, transparent)\",\n        }}\n      />\n      {/* output port (rose, hollow) — right edge */}\n      <span\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          right: -PORT / 2,\n          top: \"50%\",\n          transform: \"translateY(-50%)\",\n          width: PORT,\n          height: PORT,\n          borderRadius: 9999,\n          background: \"var(--aurora-page-bg)\",\n          border: \"2px solid var(--aurora-accent-pink)\",\n          boxShadow: \"0 0 8px color-mix(in srgb, var(--aurora-accent-pink) 45%, transparent)\",\n        }}\n      />\n    </div>\n  )\nNode.displayName = \"Node\"\n\nconst Canvas = ({ ref, title, status = \"idle\", edges = [], className, style, children, ...props }: CanvasProps & { ref?: React.Ref<HTMLDivElement> }) => {\n    const nodes = React.Children.toArray(children).filter(\n      (c): c is React.ReactElement<NodeProps> => React.isValidElement(c)\n    )\n    const boxes = nodes.map((n) => nodeBox((n.props as NodeProps).style))\n    const glow = statusGlow[status] ?? statusGlow.idle\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"relative overflow-hidden\", className)}\n        style={{\n          minHeight: 250,\n          boxSizing: \"border-box\",\n          padding: 16,\n          background: \"var(--aurora-surface-raised)\",\n          border: \"1px solid var(--aurora-border-strong)\",\n          borderRadius: \"var(--aurora-radius-2)\",\n          boxShadow: \"var(--aurora-shadow-medium), var(--aurora-highlight-medium)\",\n          backgroundImage:\n            \"radial-gradient(circle, color-mix(in srgb, var(--aurora-border-default) 70%, transparent) 1px, transparent 1px)\",\n          backgroundSize: \"22px 22px\",\n          backgroundPosition: \"10px 10px\",\n          ...style,\n        }}\n        {...props}\n      >\n        {/* edges layer */}\n        <svg\n          aria-hidden\n          style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", pointerEvents: \"none\", overflow: \"visible\" }}\n        >\n          {edges.map((e, i) => {\n            const a = boxes[e.from]\n            const b = boxes[e.to]\n            if (!a || !b) return null\n            return (\n              <path\n                key={i}\n                d={edgePath(a, b)}\n                fill=\"none\"\n                stroke=\"var(--aurora-accent-primary)\"\n                strokeWidth={2}\n                strokeLinecap=\"round\"\n                style={{ filter: \"drop-shadow(0 0 4px color-mix(in srgb, var(--aurora-accent-primary) 45%, transparent))\" }}\n              />\n            )\n          })}\n        </svg>\n\n        {/* header: status chip + node count */}\n        {(title != null || nodes.length > 0) && (\n          <div style={{ position: \"absolute\", top: 16, left: 16, right: 16, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", pointerEvents: \"none\", zIndex: 2 }}>\n            {title != null ? (\n              <div\n                style={{\n                  display: \"inline-flex\",\n                  alignItems: \"center\",\n                  gap: 9,\n                  padding: \"8px 14px\",\n                  background: \"var(--aurora-surface-raised)\",\n                  border: \"1px solid var(--aurora-border-strong)\",\n                  borderRadius: 9999,\n                  boxShadow: \"var(--aurora-shadow-medium), var(--aurora-highlight-medium)\",\n                  pointerEvents: \"auto\",\n                }}\n              >\n                <span\n                  aria-hidden\n                  style={{ width: 9, height: 9, borderRadius: 9999, background: glow.dot, boxShadow: glow.glow }}\n                />\n                <span\n                  style={{\n                    fontFamily: \"var(--aurora-font-display)\",\n                    fontWeight: 700,\n                    fontSize: 15,\n                    color: \"var(--aurora-text-primary)\",\n                  }}\n                >\n                  {title}\n                </span>\n              </div>\n            ) : (\n              <span />\n            )}\n            <div\n              style={{\n                display: \"inline-flex\",\n                alignItems: \"center\",\n                padding: \"6px 12px\",\n                fontFamily: \"var(--aurora-font-sans)\",\n                fontSize: 13,\n                fontWeight: 560,\n                color: \"var(--aurora-text-muted)\",\n                background: \"color-mix(in srgb, var(--aurora-control-surface) 60%, transparent)\",\n                border: \"1px solid var(--aurora-border-default)\",\n                borderRadius: 9999,\n                pointerEvents: \"auto\",\n              }}\n            >\n              {nodes.length} {nodes.length === 1 ? \"node\" : \"nodes\"}\n            </div>\n          </div>\n        )}\n\n        {/* nodes layer */}\n        <div style={{ position: \"relative\", zIndex: 1 }}>{children}</div>\n\n        {/* zoom toolbar */}\n        <div\n          style={{\n            position: \"absolute\",\n            bottom: 16,\n            right: 16,\n            display: \"inline-flex\",\n            alignItems: \"center\",\n            gap: 4,\n            padding: 4,\n            background: \"var(--aurora-surface-raised)\",\n            border: \"1px solid var(--aurora-border-strong)\",\n            borderRadius: \"var(--aurora-radius-1)\",\n            boxShadow: \"var(--aurora-shadow-medium), var(--aurora-highlight-medium)\",\n            zIndex: 2,\n          }}\n        >\n          <Action size=\"sm\" aria-label=\"Zoom out\">\n            <Minus aria-hidden width={16} height={16} strokeWidth={1.65} />\n          </Action>\n          <span\n            style={{\n              fontFamily: \"var(--aurora-font-sans)\",\n              fontSize: 13,\n              fontWeight: 560,\n              color: \"var(--aurora-text-primary)\",\n              minWidth: 44,\n              textAlign: \"center\",\n              fontVariantNumeric: \"tabular-nums\",\n            }}\n          >\n            100%\n          </span>\n          <Action size=\"sm\" aria-label=\"Zoom in\">\n            <Plus aria-hidden width={16} height={16} strokeWidth={1.65} />\n          </Action>\n        </div>\n      </div>\n    )\n  }\nCanvas.displayName = \"Canvas\"\n\nexport { Canvas, Node }\n",
      "type": "registry:component",
      "target": "@components/aurora/ai/canvas.tsx"
    }
  ],
  "meta": {
    "namespace": "@aurora",
    "style": "aurora",
    "family": "block",
    "requiresTokens": true,
    "sourcePath": "registry/aurora/blocks/ai/elements/canvas.tsx",
    "installTarget": "@components/aurora/ai/canvas.tsx",
    "taxonomy": "ai"
  },
  "docs": "Aurora block that installs Aurora AI Canvas.\n\nInstall `aurora-tokens` and `aurora-ai-action` first; the shadcn CLI resolves these automatically through the @aurora registry namespace.\n\nDefault install target: `@components/aurora/ai/canvas.tsx`.\n\nRegistry dependencies: `aurora-tokens`, `aurora-ai-action`.",
  "categories": [
    "aurora",
    "block",
    "ai"
  ],
  "type": "registry:block"
}