{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"board","type":"registry:ui","title":"Board","description":"Root canvas component.","package":"fancy-whiteboard","dependencies":[],"registryDependencies":["types"],"files":[{"path":"components\/fancy\/board\/Board.tsx","content":"import {\r\n  type CSSProperties,\r\n  type ReactNode,\r\n  useCallback,\r\n  useRef,\r\n  useState,\r\n} from \"react\";\r\nimport type { Viewport } from \"..\/..\/types\";\r\n\r\nexport type BoardProps = {\r\n  viewport?: Viewport;\r\n  defaultViewport?: Viewport;\r\n  onViewportChange?: (v: Viewport) => void;\r\n  minZoom?: number;\r\n  maxZoom?: number;\r\n  className?: string;\r\n  style?: CSSProperties;\r\n  children?: ReactNode;\r\n};\r\n\r\nconst DEFAULT_VIEWPORT: Viewport = { x: 0, y: 0, zoom: 1 };\r\n\r\n\/**\r\n * Board \u2014 pan\/zoom viewport that hosts whiteboard items.\r\n * Controlled (pass `viewport` + `onViewportChange`) or uncontrolled (`defaultViewport`).\r\n *\/\r\nexport function Board({\r\n  viewport: controlled,\r\n  defaultViewport = DEFAULT_VIEWPORT,\r\n  onViewportChange,\r\n  minZoom = 0.1,\r\n  maxZoom = 4,\r\n  className,\r\n  style,\r\n  children,\r\n}: BoardProps) {\r\n  const [internal, setInternal] = useState<Viewport>(defaultViewport);\r\n  const v = controlled ?? internal;\r\n  const ref = useRef<HTMLDivElement>(null);\r\n\r\n  const update = useCallback(\r\n    (next: Viewport) => {\r\n      if (controlled === undefined) setInternal(next);\r\n      onViewportChange?.(next);\r\n    },\r\n    [controlled, onViewportChange],\r\n  );\r\n\r\n  const onWheel = useCallback(\r\n    (e: React.WheelEvent) => {\r\n      if (!e.ctrlKey && !e.metaKey) return;\r\n      e.preventDefault();\r\n      const rect = ref.current?.getBoundingClientRect();\r\n      if (!rect) return;\r\n      const px = e.clientX - rect.left;\r\n      const py = e.clientY - rect.top;\r\n      const factor = Math.exp(-e.deltaY * 0.001);\r\n      const nextZoom = Math.min(maxZoom, Math.max(minZoom, v.zoom * factor));\r\n      const k = nextZoom \/ v.zoom;\r\n      update({ x: px - (px - v.x) * k, y: py - (py - v.y) * k, zoom: nextZoom });\r\n    },\r\n    [v, update, minZoom, maxZoom],\r\n  );\r\n\r\n  const onPointerDown = useCallback(\r\n    (e: React.PointerEvent) => {\r\n      if (e.button !== 1 && !(e.button === 0 && e.altKey)) return;\r\n      e.preventDefault();\r\n      const start = { x: e.clientX, y: e.clientY };\r\n      const startV = v;\r\n      const target = e.currentTarget;\r\n      target.setPointerCapture(e.pointerId);\r\n      const move = (ev: PointerEvent) => {\r\n        update({ x: startV.x + ev.clientX - start.x, y: startV.y + ev.clientY - start.y, zoom: startV.zoom });\r\n      };\r\n      const up = () => {\r\n        window.removeEventListener(\"pointermove\", move);\r\n        window.removeEventListener(\"pointerup\", up);\r\n      };\r\n      window.addEventListener(\"pointermove\", move);\r\n      window.addEventListener(\"pointerup\", up);\r\n    },\r\n    [v, update],\r\n  );\r\n\r\n  return (\r\n    <div\r\n      ref={ref}\r\n      className={className ? `fw-board ${className}` : \"fw-board\"}\r\n      style={style}\r\n      onWheel={onWheel}\r\n      onPointerDown={onPointerDown}\r\n    >\r\n      <div\r\n        className=\"fw-board__viewport\"\r\n        style={{ transform: `translate(${v.x}px, ${v.y}px) scale(${v.zoom})` }}\r\n      >\r\n        {children}\r\n      <\/div>\r\n    <\/div>\r\n  );\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/board\/Board.tsx"},{"path":"components\/fancy\/board\/index.ts","content":"export { Board } from \".\/Board\";\r\nexport type { BoardProps } from \".\/Board\";\r\n","type":"registry:ui","target":"components\/fancy\/board\/index.ts"}]}