{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "snap-guides",
  "type": "registry:ui",
  "title": "Snap guides",
  "description": "The lines that explain a snap while a person moves or resizes on the canvas: 1 pt intent green, solid for aligned edges, dashed for aligned centres, spanning the aligned objects plus 8 pt, the same on screen at every zoom.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/snap-guides/snap-guides.tsx",
      "type": "registry:ui",
      "target": "components/metalui/snap-guides/snap-guides.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\n/* ─────────────────────────────────────────────────────────\n * SNAP GUIDES (the native reference's guides overlay)\n *\n * While a person moves or resizes, the canvas snaps to its neighbours' edges and centres.\n * Each guide is the explanation of one snap.\n *\n *   rest      nothing\n *   snapping  a 1 pt intent-green line per alignment, spanning every aligned object plus\n *             8 pt at each end; solid for edges, 3 / 3 dashed for centres. The same width\n *             and dash at every zoom (the host passes the canvas scale). Guides appear and\n *             move with the snap in the same frame, never animated: a guide that lagged\n *             would contradict the snap it explains.\n *   release   the drag ends: the last guides fade on the release spring, then clear\n *   graphite  the lighter green, so the line reads on the dark world\n *   haptic    onEngage fires once when a snap catches a new line; staying on a line, or\n *             letting go, is silent (the native reference's gate). The Mac plays the\n *             trackpad's alignment tap on it; browsers on a Mac or an iPhone have no\n *             haptics, so the web host may only vibrate where the browser allows it.\n * Drawn in world coordinates, inside the transformed world, as one path for edges and one\n * for centres. Not interactive and hidden from assistive tech: the snap is the fact.\n * ───────────────────────────────────────────────────────── */\n\nexport interface SnapGuide {\n  /** vertical: a line at x = position from y = start to end. horizontal: at y = position. */\n  axis: 'vertical' | 'horizontal';\n  /** World coordinate of the line. */\n  position: number;\n  /** World extent of the aligned objects along the line (the overshoot is added here). */\n  start: number;\n  end: number;\n  /** edge: aligned edges (solid). center: aligned centres (dashed). */\n  kind: 'edge' | 'center';\n}\n\nexport interface SnapGuidesProps {\n  /** The guides for this frame; an empty list ends the drag and lets the last ones fade. */\n  guides: SnapGuide[];\n  /** The canvas scale (1 at 100 %), so lines keep their screen width, dash and overshoot. */\n  scale?: number;\n  /** Fires once when a snap catches a new line (for the haptic). Staying on a line, or letting go, is silent. */\n  onEngage?: () => void;\n  className?: string;\n}\n\n/** The identity of an engaged line: its axis and position to a hundredth. */\nconst keyOf = (g: SnapGuide) => `${g.axis}:${Math.round(g.position * 100) / 100}`;\n\nconst LAYER = 'mu-snap-guides pointer-events-none absolute left-0 top-0 overflow-visible';\nconst LINE = 'presence-guide-line';\nconst CENTER = 'presence-guide-line presence-guide-center';\n\n/** The overshoot in screen points, from the theme (8 by default). */\nfunction overshoot() {\n  if (typeof window === 'undefined') return 8;\n  return parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--mu-presence-guide-overshoot')) || 8;\n}\n\nfunction pathOf(guides: SnapGuide[], kind: SnapGuide['kind'], over: number) {\n  let d = '';\n  for (const g of guides) {\n    if (g.kind !== kind) continue;\n    const a = Math.min(g.start, g.end) - over, b = Math.max(g.start, g.end) + over;\n    d += g.axis === 'vertical' ? `M${g.position} ${a}V${b}` : `M${a} ${g.position}H${b}`;\n  }\n  return d;\n}\n\n/** The snap guides of one drag, drawn inside the canvas world. */\nexport function SnapGuides({ guides, scale = 1, onEngage, className }: SnapGuidesProps) {\n  // The haptic gate: a line that was not engaged in the last frame is a new catch.\n  const engaged = React.useRef<Set<string>>(new Set());\n  React.useLayoutEffect(() => {\n    const keys = new Set(guides.map(keyOf));\n    if (onEngage && [...keys].some((k) => !engaged.current.has(k))) onEngage();\n    engaged.current = keys;\n  }, [guides, onEngage]);\n  // Keep the last guides while they fade out after the drag ends.\n  const [shown, setShown] = React.useState<SnapGuide[]>(guides);\n  const leaving = guides.length === 0 && shown.length > 0;\n  React.useLayoutEffect(() => {\n    if (guides.length) setShown(guides);\n  }, [guides]);\n  // After the fade (or at once under Reduce Motion, where nothing fades), the guides are gone.\n  React.useEffect(() => {\n    if (!leaving) return;\n    const t = window.setTimeout(() => setShown([]), 400);\n    return () => window.clearTimeout(t);\n  }, [leaving]);\n  const over = React.useMemo(overshoot, []) / scale;\n  const draw = guides.length ? guides : shown;\n  if (!draw.length) return null;\n  return (\n    <svg\n      aria-hidden\n      width={1}\n      height={1}\n      data-state={leaving ? 'leaving' : 'snapping'}\n      className={[LAYER, 'presence-guide-leave data-[state=leaving]:opacity-0 data-[state=snapping]:transition-none reduced-motion:transition-none', className].filter(Boolean).join(' ')}\n      style={{ '--mu-canvas-scale': scale } as React.CSSProperties}\n    >\n      <path className={LINE} d={pathOf(draw, 'edge', over)} />\n      <path className={CENTER} d={pathOf(draw, 'center', over)} />\n    </svg>\n  );\n}\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/snap-guides.md. SwiftUI: MetalSnapGuides in the MetalUI Swift package."
}
