{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "connector",
  "type": "registry:ui",
  "title": "Connector",
  "description": "The look around ink that joins two blocks: a green halo and end dots on hover (solid attached, hollow free), end handles when selected, and a label chip at the middle; same size on screen at every zoom.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/connector/connector.tsx",
      "type": "registry:ui",
      "target": "components/metalui/connector/connector.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\n/* ─────────────────────────────────────────────────────────\n * CONNECTOR (DRAWING.md DR-07): a line between two blocks, and everything around it\n *\n * Looks (what the line is made of):\n *   elastic   the default. A taut band: its middle rides a spring toward the true\n *             middle, so when a block moves the line bends behind it and whips back\n *             straight with one overshoot. Arrowheads show the flow.\n *   current   light flows along a quiet line: comets leave the source slowly, speed\n *             up, and slow down into the target, which glows as each one lands.\n *             The flow quickens while a block moves, then calms.\n *   stardust  the line is a trail of drifting, twinkling motes; a shimmer runs in the\n *             flow's direction. Hover or drag: the motes pull into a line.\n * Flow: forward (from → to), backward (to → from) or both.\n *\n * Chrome (the same for every look):\n *   rest      the line and its label chip\n *   hover     a soft green halo along the path, and a dot at each end: solid where the\n *             end is on a block, hollow where it is free (fades in on the part spring)\n *   selected  the halo stays; the ends become handles to drag and re-attach\n *   label     a chip at the middle of the line, the same size on screen at every zoom\n *\n * The line is world ink (it scales with zoom); the chrome keeps its screen size.\n * Reduce Motion: no spring, no flow; elastic's straight line with its arrowheads.\n * Current and stardust animate all the time: use them to show a flow, not for every line.\n * ───────────────────────────────────────────────────────── */\n\nexport interface ConnectorEnd {\n  x: number;\n  y: number;\n  /** On a block (solid dot) or free (hollow dot). */\n  attached: boolean;\n}\n\nexport type ConnectorLook = 'elastic' | 'current' | 'stardust';\nexport type ConnectorFlow = 'forward' | 'backward' | 'both';\n\nexport interface ConnectorProps {\n  from: ConnectorEnd;\n  to: ConnectorEnd;\n  look?: ConnectorLook;\n  flow?: ConnectorFlow;\n  /** The ink colour (see inkColor), and its width in world units. */\n  ink?: string;\n  width?: number;\n  state?: 'rest' | 'hover' | 'selected';\n  label?: string;\n  /** The canvas scale (1 at 100 %), so the chrome keeps its screen size. */\n  scale?: number;\n  /** An end handle was pressed (selected only). The host drags it and re-attaches. */\n  onEndPointerDown?: (end: 'from' | 'to', event: React.PointerEvent<SVGCircleElement>) => void;\n  /** The pointer came onto or left the line (a band 18 wide on screen). */\n  onHoverChange?: (hovered: boolean) => void;\n  /** The line was pressed: the host selects it. */\n  onPress?: (event: React.PointerEvent<SVGPathElement>) => void;\n  className?: string;\n}\n\n/* The physics and the flow, as named numbers. */\nconst BAND = { k: 170, damping: 13, rest: 0.05 };          // the middle's spring; settled under .05 pt\nconst HEAD = 3.2;                                           // arrowhead length, in line widths + 5\nconst COMET = { perDirection: 3, speed: 0.38, tail: 16, step: 0.014, boost: 0.004, calm: 2.4 };\nconst BLOOM = { rise: 0.8, fall: 0.14 };                    // the target glows as a comet lands\nconst DUST = { motes: 34, drift: 3.4, shimmer: 0.5, align: { k: 120, damping: 14 } };\n\nconst LAYER = 'mu-connector connector-layer';\nconst HALO = 'mu-connector-halo connector-halo';\nconst CHROME = 'mu-connector-chrome connector-chrome';\nconst END = { attached: 'connector-end', free: 'connector-end-free' };\nconst HANDLE = 'mu-connector-handle connector-handle';\nconst LABEL = 'mu-connector-label connector-label';\nconst GLOW = 'connector-glow';\nconst HIT = 'mu-connector-hit connector-hit';\n\ntype P = { x: number; y: number };\n\n/** A size in screen points from the theme. */\nfunction cssPx(name: string, fallback: number) {\n  if (typeof window === 'undefined') return fallback;\n  return parseFloat(getComputedStyle(document.documentElement).getPropertyValue(name)) || fallback;\n}\n/** A point on the curve from a to b that passes through m at its middle. */\nfunction onBand(a: P, m: P, b: P, t: number): P {\n  const qx = 2 * m.x - (a.x + b.x) / 2, qy = 2 * m.y - (a.y + b.y) / 2, u = 1 - t;\n  return { x: u * u * a.x + 2 * u * t * qx + t * t * b.x, y: u * u * a.y + 2 * u * t * qy + t * t * b.y };\n}\nconst bandPath = (a: P, m: P, b: P) => `M${a.x} ${a.y}Q${2 * m.x - (a.x + b.x) / 2} ${2 * m.y - (a.y + b.y) / 2} ${b.x} ${b.y}`;\nfunction arrowhead(tip: P, from: P, len: number) {\n  const g = Math.atan2(tip.y - from.y, tip.x - from.x);\n  return `M${tip.x - len * Math.cos(g - 0.5)} ${tip.y - len * Math.sin(g - 0.5)}L${tip.x} ${tip.y}L${tip.x - len * Math.cos(g + 0.5)} ${tip.y - len * Math.sin(g + 0.5)}`;\n}\nconst frac = (v: number) => ((v % 1) + 1) % 1;\nconst easeInOut = (u: number) => 0.5 - 0.5 * Math.cos(Math.PI * u);\n\nfunction useReducedMotion() {\n  const [reduce, setReduce] = React.useState(false);\n  React.useEffect(() => {\n    const q = window.matchMedia('(prefers-reduced-motion: reduce)');\n    setReduce(q.matches);\n    const on = () => setReduce(q.matches);\n    q.addEventListener('change', on);\n    return () => q.removeEventListener('change', on);\n  }, []);\n  return reduce;\n}\n\n/** A line between two blocks, with its look, its flow and its chrome. */\nexport function Connector({\n  from, to, look = 'elastic', flow = 'forward', ink = 'currentColor', width = 2, state = 'rest', label, scale = 1, onEndPointerDown, onHoverChange, onPress, className,\n}: ConnectorProps) {\n  const reduce = useReducedMotion();\n  const endR = React.useMemo(() => cssPx('--mu-r-connector-end-size', 4.5), []) / scale;\n  const handleR = React.useMemo(() => cssPx('--mu-r-connector-handle-size', 5), []) / scale;\n  const glowId = `mu-connector-glow-${React.useId().replace(/:/g, '')}`;\n\n  // The live world: the latest ends, the spring middle, the flow clock.\n  const live = React.useRef({ from, to, state, look });\n  live.current = { from, to, state, look };\n  const sim = React.useRef({ m: { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2, vx: 0, vy: 0 }, time: 0, phase: 0, energy: 0, align: 0, av: 0, last: { ...from, tx: to.x, ty: to.y } });\n  const [, redraw] = React.useReducer((n: number) => n + 1, 0);\n\n  React.useEffect(() => {\n    if (reduce) return;\n    let raf = 0, prev = performance.now();\n    const loop = (now: number) => {\n      const dt = Math.min(1 / 30, (now - prev) / 1000); prev = now;\n      const s = sim.current, { from: a, to: b, state: st, look: lk } = live.current;\n      const tx = (a.x + b.x) / 2, ty = (a.y + b.y) / 2, m = s.m;\n      m.vx += (BAND.k * (tx - m.x) - BAND.damping * m.vx) * dt; m.vy += (BAND.k * (ty - m.y) - BAND.damping * m.vy) * dt;\n      m.x += m.vx * dt; m.y += m.vy * dt;\n      const moved = Math.hypot(a.x - s.last.x, a.y - s.last.y) + Math.hypot(b.x - s.last.tx, b.y - s.last.ty);\n      s.last = { ...a, tx: b.x, ty: b.y };\n      s.energy = Math.max(0, s.energy + moved * COMET.boost - s.energy * COMET.calm * dt);\n      s.phase += (COMET.speed + s.energy) * dt;\n      s.time += dt;\n      const want = st !== 'rest' ? 1 : 0;\n      s.av += (DUST.align.k * (want - s.align) - DUST.align.damping * s.av) * dt; s.align += s.av * dt;\n      const settled = Math.hypot(tx - m.x, ty - m.y) < BAND.rest && Math.hypot(m.vx, m.vy) < BAND.rest;\n      if (settled) { m.x = tx; m.y = ty; m.vx = m.vy = 0; }\n      // Elastic rests when the spring does; the flow looks keep going.\n      if (!settled || lk !== 'elastic') redraw();\n      raf = requestAnimationFrame(loop);\n    };\n    raf = requestAnimationFrame(loop);\n    return () => cancelAnimationFrame(raf);\n  }, [reduce]);\n\n  const s = sim.current;\n  const mid: P = reduce ? { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 } : s.m;\n  const d = bandPath(from, mid, to);\n  const fwd = flow !== 'backward', back = flow !== 'forward';\n  const vars = { '--mu-canvas-scale': scale } as React.CSSProperties;\n  const selected = state === 'selected';\n  const line = { fill: 'none', stroke: ink, strokeWidth: width, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const };\n\n  const art = (() => {\n    if (look === 'elastic' || reduce) {\n      const len = width * HEAD + 5;\n      const heads = (fwd ? arrowhead(to, onBand(from, mid, to, 0.92), len) : '') + (back ? arrowhead(from, onBand(from, mid, to, 0.08), len) : '');\n      return <path {...line} d={d + heads} />;\n    }\n    if (look === 'current') {\n      const dirs = [fwd && 1, back && -1].filter(Boolean) as number[];\n      let bloomTo = 0, bloomFrom = 0;\n      const comets = dirs.flatMap((dir, di) =>\n        Array.from({ length: COMET.perDirection }, (_, i) => {\n          const u = frac(s.phase + i / COMET.perDirection + di / (COMET.perDirection * 2));\n          const speed = Math.sin(Math.PI * u), fade = Math.sqrt(speed);\n          const bloom = u > BLOOM.rise ? (u - BLOOM.rise) / (1 - BLOOM.rise) : u < BLOOM.fall ? 1 - u / BLOOM.fall : 0;\n          if (dir > 0) bloomTo = Math.max(bloomTo, bloom); else bloomFrom = Math.max(bloomFrom, bloom);\n          const e = easeInOut(u), head = dir > 0 ? e : 1 - e;\n          const tail = Array.from({ length: COMET.tail }, (_, j) => {\n            // Motion blur: the tail stretches with the comet's speed.\n            const t = head - dir * j * COMET.step * (0.25 + 1.1 * speed);\n            if (t < 0 || t > 1) return null;\n            const p = onBand(from, mid, to, t), k = 1 - j / COMET.tail;\n            return <circle key={j} cx={p.x} cy={p.y} r={width * (0.4 + 0.9 * k)} className={GLOW} opacity={fade * Math.pow(k, 1.4)} />;\n          });\n          const h = onBand(from, mid, to, head);\n          return (\n            <g key={`${dir}-${i}`}>\n              <circle cx={h.x} cy={h.y} r={width * 3.2} className={GLOW} opacity={fade * 0.8} filter={`url(#${glowId})`} />\n              {tail}\n              <circle cx={h.x} cy={h.y} r={width * 0.85} fill={ink} opacity={fade} />\n            </g>\n          );\n        }),\n      );\n      return (\n        <>\n          <path {...line} d={d} strokeOpacity={0.3} />\n          {comets}\n          {bloomTo > 0 && <circle cx={to.x} cy={to.y} r={width * (2 + bloomTo * 2.5)} className={GLOW} opacity={bloomTo * 0.8} filter={`url(#${glowId})`} />}\n          {bloomFrom > 0 && <circle cx={from.x} cy={from.y} r={width * (2 + bloomFrom * 2.5)} className={GLOW} opacity={bloomFrom * 0.8} filter={`url(#${glowId})`} />}\n        </>\n      );\n    }\n    // stardust\n    const loose = 1 - Math.max(0, Math.min(1.1, s.align));\n    return Array.from({ length: DUST.motes }, (_, i) => {\n      const t = i / (DUST.motes - 1), p = onBand(from, mid, to, t), q = onBand(from, mid, to, Math.min(1, t + 0.01));\n      const tl = Math.hypot(q.x - p.x, q.y - p.y) || 1, nx = -(q.y - p.y) / tl, ny = (q.x - p.x) / tl;\n      const w = (Math.sin(s.time * 1.3 + i * 2.1) + Math.sin(s.time * 0.7 + i * 5.3) * 0.6) * DUST.drift * loose;\n      const along = Math.sin(s.time * 0.9 + i * 3.7) * 0.5 * DUST.drift * loose;\n      const wave = (pos: number) => Math.max(0, 1 - Math.abs(frac(pos - s.time * DUST.shimmer) - 0.5) * 7);\n      const shine = Math.max(fwd ? wave(t) : 0, back ? wave(1 - t) : 0);\n      const twinkle = 0.45 + 0.35 * (0.5 + 0.5 * Math.sin(s.time * 2.3 + i * 1.7));\n      return (\n        <circle\n          key={i}\n          cx={p.x + nx * w + ((q.x - p.x) / tl) * along}\n          cy={p.y + ny * w + ((q.y - p.y) / tl) * along}\n          r={width * (0.62 + shine * 0.45)}\n          className={shine > 0.2 ? GLOW : undefined}\n          fill={shine > 0.2 ? undefined : ink}\n          opacity={Math.min(1, twinkle + shine * 0.6)}\n        />\n      );\n    });\n  })();\n\n  const labelAt = look === 'elastic' || reduce ? onBand(from, mid, to, 0.5) : mid;\n\n  return (\n    <>\n      <svg aria-hidden width={1} height={1} data-state={state} data-look={look} className={className ? `${LAYER} ${className}` : LAYER} style={vars}>\n        <defs>\n          <filter id={glowId} x=\"-100%\" y=\"-100%\" width=\"300%\" height=\"300%\"><feGaussianBlur stdDeviation={width * 1.4} /></filter>\n        </defs>\n        {art}\n        <path className={HIT} d={d} onPointerEnter={() => onHoverChange?.(true)} onPointerLeave={() => onHoverChange?.(false)} onPointerDown={onPress} />\n        <g className={CHROME}>\n          <path className={HALO} d={d} />\n          {(['from', 'to'] as const).map((k) => {\n            const e = k === 'from' ? from : to;\n            return selected ? (\n              <circle key={k} className={HANDLE} cx={e.x} cy={e.y} r={handleR} onPointerDown={(ev) => onEndPointerDown?.(k, ev)} />\n            ) : (\n              <circle key={k} className={END[e.attached ? 'attached' : 'free']} cx={e.x} cy={e.y} r={endR} />\n            );\n          })}\n        </g>\n      </svg>\n      {label && (\n        <span className={LABEL} style={{ ...vars, left: labelAt.x, top: labelAt.y }}>\n          {label}\n        </span>\n      )}\n    </>\n  );\n}\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/connector.md. SwiftUI: MetalConnector in the MetalUI Swift package."
}
