{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"drawing","type":"registry:ui","title":"Drawing","description":"Freeform pen strokes.","package":"fancy-whiteboard","dependencies":[],"registryDependencies":["types"],"files":[{"path":"components\/fancy\/drawing\/Drawing.tsx","content":"import { type CSSProperties, useCallback, useEffect, useRef, useState } from \"react\";\r\nimport type { Point, Stroke } from \"..\/..\/types\";\r\n\r\nexport type DrawingProps = {\r\n  strokes: Stroke[];\r\n  onStrokeStart?: (stroke: Stroke) => void;\r\n  onStrokePoint?: (id: string, point: Point) => void;\r\n  onStrokeEnd?: (stroke: Stroke) => void;\r\n  color?: string;\r\n  size?: number;\r\n  \/** SVG canvas width. Optional \u2014 auto-measures the rendered element when omitted. *\/\r\n  width?: number;\r\n  \/** SVG canvas height. Optional \u2014 auto-measures the rendered element when omitted. *\/\r\n  height?: number;\r\n  enabled?: boolean;\r\n  className?: string;\r\n  style?: CSSProperties;\r\n};\r\n\r\n\/**\r\n * Drawing \u2014 freeform pen layer. Controlled: parent owns `strokes`.\r\n *\r\n * Coordinates are in screen pixels relative to the rendered SVG. By default\r\n * the component auto-measures itself with a ResizeObserver so width\/height\r\n * always match the visible area, keeping user coords 1:1 with click position.\r\n *\r\n * Streams new strokes via the callbacks so apps can broadcast them as they\r\n * form (live multi-user pen view).\r\n *\/\r\nexport function Drawing({\r\n  strokes,\r\n  onStrokeStart,\r\n  onStrokePoint,\r\n  onStrokeEnd,\r\n  color = \"#111827\",\r\n  size = 2,\r\n  width,\r\n  height,\r\n  enabled = true,\r\n  className,\r\n  style,\r\n}: DrawingProps) {\r\n  const [active, setActive] = useState<Stroke | null>(null);\r\n  const [measured, setMeasured] = useState({ w: width ?? 0, h: height ?? 0 });\r\n  const ref = useRef<SVGSVGElement>(null);\r\n\r\n  \/\/ Auto-measure rendered size so coords map 1:1 to clicks regardless of\r\n  \/\/ CSS sizing. Skipped when caller supplied explicit width AND height.\r\n  useEffect(() => {\r\n    if (width != null && height != null) return;\r\n    const el = ref.current;\r\n    if (!el) return;\r\n    const update = () => {\r\n      const r = el.getBoundingClientRect();\r\n      setMeasured({ w: Math.max(1, Math.round(r.width)), h: Math.max(1, Math.round(r.height)) });\r\n    };\r\n    update();\r\n    const ro = new ResizeObserver(update);\r\n    ro.observe(el);\r\n    return () => ro.disconnect();\r\n  }, [width, height]);\r\n\r\n  const w = width ?? measured.w;\r\n  const h = height ?? measured.h;\r\n\r\n  const localPoint = useCallback((e: React.PointerEvent | PointerEvent): Point => {\r\n    const rect = ref.current?.getBoundingClientRect();\r\n    return { x: e.clientX - (rect?.left ?? 0), y: e.clientY - (rect?.top ?? 0) };\r\n  }, []);\r\n\r\n  const onPointerDown = useCallback(\r\n    (e: React.PointerEvent) => {\r\n      if (!enabled || e.button !== 0) return;\r\n      e.preventDefault();\r\n      const id = `s_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;\r\n      const stroke: Stroke = { id, points: [localPoint(e)], color, size };\r\n      setActive(stroke);\r\n      onStrokeStart?.(stroke);\r\n      const target = e.currentTarget;\r\n      target.setPointerCapture(e.pointerId);\r\n      const move = (ev: PointerEvent) => {\r\n        const p = localPoint(ev);\r\n        setActive((prev) => (prev ? { ...prev, points: [...prev.points, p] } : prev));\r\n        onStrokePoint?.(id, p);\r\n      };\r\n      const up = () => {\r\n        window.removeEventListener(\"pointermove\", move);\r\n        window.removeEventListener(\"pointerup\", up);\r\n        setActive((prev) => {\r\n          if (prev) onStrokeEnd?.(prev);\r\n          return null;\r\n        });\r\n      };\r\n      window.addEventListener(\"pointermove\", move);\r\n      window.addEventListener(\"pointerup\", up);\r\n    },\r\n    [enabled, color, size, localPoint, onStrokeStart, onStrokePoint, onStrokeEnd],\r\n  );\r\n\r\n  const all = active ? [...strokes, active] : strokes;\r\n\r\n  return (\r\n    <svg\r\n      ref={ref}\r\n      className={className}\r\n      style={{ pointerEvents: enabled ? \"auto\" : \"none\", ...style }}\r\n      width={w || undefined}\r\n      height={h || undefined}\r\n      onPointerDown={onPointerDown}\r\n    >\r\n      {\/* Transparent hit-target so empty SVG area catches pointer events. *\/}\r\n      {enabled && w > 0 && h > 0 && (\r\n        <rect x={0} y={0} width={w} height={h} fill=\"transparent\" \/>\r\n      )}\r\n      {all.map((s) => (\r\n        <path\r\n          key={s.id}\r\n          d={toPath(s.points)}\r\n          fill=\"none\"\r\n          stroke={s.color ?? \"#111827\"}\r\n          strokeWidth={s.size ?? 2}\r\n          strokeLinecap=\"round\"\r\n          strokeLinejoin=\"round\"\r\n        \/>\r\n      ))}\r\n    <\/svg>\r\n  );\r\n}\r\n\r\nfunction toPath(points: Point[]): string {\r\n  if (!points.length) return \"\";\r\n  return points.map((p, i) => `${i === 0 ? \"M\" : \"L\"}${p.x},${p.y}`).join(\" \");\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/drawing\/Drawing.tsx"},{"path":"components\/fancy\/drawing\/index.ts","content":"export { Drawing } from \".\/Drawing\";\r\nexport type { DrawingProps } from \".\/Drawing\";\r\n","type":"registry:ui","target":"components\/fancy\/drawing\/index.ts"}]}