{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"shape","type":"registry:ui","title":"Shape","description":"Geometric shapes.","package":"fancy-whiteboard","dependencies":[],"registryDependencies":["types"],"files":[{"path":"components\/fancy\/shape\/Shape.tsx","content":"import { type CSSProperties, useState } from \"react\";\r\nimport type { ShapeItem } from \"..\/..\/types\";\r\n\r\nexport type ShapeProps = {\r\n  item: ShapeItem;\r\n  onChange?: (next: ShapeItem) => void;\r\n  onSelect?: (id: string) => void;\r\n  selected?: boolean;\r\n  readOnly?: boolean;\r\n  className?: string;\r\n  style?: CSSProperties;\r\n};\r\n\r\nconst DEFAULT_FILL = \"rgba(248, 250, 252, 0.85)\";\r\nconst DEFAULT_STROKE = \"#334155\";\r\nconst DEFAULT_STROKE_W = 2;\r\n\r\n\/**\r\n * Shape \u2014 basic whiteboard primitives. All shapes share a bounding-box model\r\n * (x, y, width, height); the `shape` discriminator picks the renderer.\r\n *\r\n * Supported kinds: rect, rounded-rect, ellipse, diamond, triangle, line,\r\n * arrow, text. Drag-to-move is built in.\r\n *\/\r\nexport function Shape({\r\n  item,\r\n  onChange,\r\n  onSelect,\r\n  selected,\r\n  readOnly,\r\n  className,\r\n  style,\r\n}: ShapeProps) {\r\n  const [dragging, setDragging] = useState(false);\r\n\r\n  const onPointerDown = (e: React.PointerEvent) => {\r\n    if (readOnly || !onChange) return;\r\n    if (e.button !== 0 || e.altKey) return;\r\n    onSelect?.(item.id);\r\n    const start = { x: e.clientX, y: e.clientY };\r\n    const origin = { x: item.x, y: item.y };\r\n    const target = e.currentTarget;\r\n    target.setPointerCapture(e.pointerId);\r\n    let moved = false;\r\n    const move = (ev: PointerEvent) => {\r\n      if (!moved) setDragging(true);\r\n      moved = true;\r\n      onChange({ ...item, x: origin.x + ev.clientX - start.x, y: origin.y + ev.clientY - start.y });\r\n    };\r\n    const up = () => {\r\n      window.removeEventListener(\"pointermove\", move);\r\n      window.removeEventListener(\"pointerup\", up);\r\n      setDragging(false);\r\n    };\r\n    window.addEventListener(\"pointermove\", move);\r\n    window.addEventListener(\"pointerup\", up);\r\n  };\r\n\r\n  const baseStyle: CSSProperties = {\r\n    left: item.x,\r\n    top: item.y,\r\n    width: item.width,\r\n    height: item.height,\r\n    zIndex: item.z,\r\n    ...style,\r\n  };\r\n\r\n  const cls = [\r\n    \"fw-item fw-shape\",\r\n    dragging ? \"fw-item--dragging\" : \"\",\r\n    selected ? \"fw-shape--selected\" : \"\",\r\n    className ?? \"\",\r\n  ].filter(Boolean).join(\" \");\r\n\r\n  if (item.shape === \"text\") {\r\n    return (\r\n      <div\r\n        className={`${cls} fw-shape--text`}\r\n        style={baseStyle}\r\n        onPointerDown={onPointerDown}\r\n      >\r\n        {item.text ?? \"\"}\r\n      <\/div>\r\n    );\r\n  }\r\n\r\n  return (\r\n    <svg\r\n      className={cls}\r\n      style={{ ...baseStyle, overflow: \"visible\" }}\r\n      width={item.width}\r\n      height={item.height}\r\n      onPointerDown={onPointerDown}\r\n    >\r\n      <ShapeGeometry item={item} \/>\r\n      {item.text && item.shape !== \"line\" && item.shape !== \"arrow\" && (\r\n        <text\r\n          x={item.width \/ 2}\r\n          y={item.height \/ 2}\r\n          textAnchor=\"middle\"\r\n          dominantBaseline=\"middle\"\r\n          fontFamily=\"ui-sans-serif, system-ui, sans-serif\"\r\n          fontSize={14}\r\n          fill=\"#1f2937\"\r\n          style={{ pointerEvents: \"none\" }}\r\n        >\r\n          {item.text}\r\n        <\/text>\r\n      )}\r\n    <\/svg>\r\n  );\r\n}\r\n\r\n\/**\r\n * Compute the from\/to local-coord endpoints for line and arrow shapes,\r\n * honoring `flipX`\/`flipY` so the shape preserves the drag direction\r\n * the user actually drew.\r\n *\/\r\nfunction lineEndpoints(item: ShapeItem): [{ x: number; y: number }, { x: number; y: number }] {\r\n  const { width: w, height: h, flipX, flipY } = item;\r\n  const fromX = flipX ? w : 0;\r\n  const fromY = flipY ? h : 0;\r\n  const toX = flipX ? 0 : w;\r\n  const toY = flipY ? 0 : h;\r\n  return [{ x: fromX, y: fromY }, { x: toX, y: toY }];\r\n}\r\n\r\nfunction ShapeGeometry({ item }: { item: ShapeItem }) {\r\n  const { width: w, height: h } = item;\r\n  const fill = item.fill ?? DEFAULT_FILL;\r\n  const stroke = item.stroke ?? DEFAULT_STROKE;\r\n  const sw = DEFAULT_STROKE_W;\r\n  const inset = sw \/ 2 + 0.5;\r\n\r\n  switch (item.shape) {\r\n    case \"rect\":\r\n      return (\r\n        <rect\r\n          x={inset}\r\n          y={inset}\r\n          width={Math.max(0, w - inset * 2)}\r\n          height={Math.max(0, h - inset * 2)}\r\n          fill={fill}\r\n          stroke={stroke}\r\n          strokeWidth={sw}\r\n        \/>\r\n      );\r\n    case \"rounded-rect\":\r\n      return (\r\n        <rect\r\n          x={inset}\r\n          y={inset}\r\n          width={Math.max(0, w - inset * 2)}\r\n          height={Math.max(0, h - inset * 2)}\r\n          rx={Math.min(16, Math.min(w, h) \/ 4)}\r\n          ry={Math.min(16, Math.min(w, h) \/ 4)}\r\n          fill={fill}\r\n          stroke={stroke}\r\n          strokeWidth={sw}\r\n        \/>\r\n      );\r\n    case \"ellipse\":\r\n      return (\r\n        <ellipse\r\n          cx={w \/ 2}\r\n          cy={h \/ 2}\r\n          rx={Math.max(0, w \/ 2 - inset)}\r\n          ry={Math.max(0, h \/ 2 - inset)}\r\n          fill={fill}\r\n          stroke={stroke}\r\n          strokeWidth={sw}\r\n        \/>\r\n      );\r\n    case \"diamond\": {\r\n      const pts = `${w \/ 2},${inset} ${w - inset},${h \/ 2} ${w \/ 2},${h - inset} ${inset},${h \/ 2}`;\r\n      return <polygon points={pts} fill={fill} stroke={stroke} strokeWidth={sw} \/>;\r\n    }\r\n    case \"triangle\": {\r\n      const pts = `${w \/ 2},${inset} ${w - inset},${h - inset} ${inset},${h - inset}`;\r\n      return <polygon points={pts} fill={fill} stroke={stroke} strokeWidth={sw} \/>;\r\n    }\r\n    case \"line\": {\r\n      const [from, to] = lineEndpoints(item);\r\n      return (\r\n        <line\r\n          x1={from.x}\r\n          y1={from.y}\r\n          x2={to.x}\r\n          y2={to.y}\r\n          stroke={stroke}\r\n          strokeWidth={sw}\r\n          strokeLinecap=\"round\"\r\n        \/>\r\n      );\r\n    }\r\n    case \"arrow\": {\r\n      const [from, to] = lineEndpoints(item);\r\n      const headSize = Math.max(8, sw * 5);\r\n      const dx = to.x - from.x;\r\n      const dy = to.y - from.y;\r\n      const len = Math.hypot(dx, dy) || 1;\r\n      const ux = dx \/ len;\r\n      const uy = dy \/ len;\r\n      const baseX = to.x - ux * headSize;\r\n      const baseY = to.y - uy * headSize;\r\n      const perpX = -uy;\r\n      const perpY = ux;\r\n      const leftX = baseX + perpX * headSize * 0.5;\r\n      const leftY = baseY + perpY * headSize * 0.5;\r\n      const rightX = baseX - perpX * headSize * 0.5;\r\n      const rightY = baseY - perpY * headSize * 0.5;\r\n      return (\r\n        <g>\r\n          <line x1={from.x} y1={from.y} x2={baseX} y2={baseY} stroke={stroke} strokeWidth={sw} strokeLinecap=\"round\" \/>\r\n          <polygon points={`${to.x},${to.y} ${leftX},${leftY} ${rightX},${rightY}`} fill={stroke} stroke={stroke} strokeWidth={sw} strokeLinejoin=\"round\" \/>\r\n        <\/g>\r\n      );\r\n    }\r\n    default:\r\n      return null;\r\n  }\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/shape\/Shape.tsx"},{"path":"components\/fancy\/shape\/index.ts","content":"export { Shape } from \".\/Shape\";\r\nexport type { ShapeProps } from \".\/Shape\";\r\n","type":"registry:ui","target":"components\/fancy\/shape\/index.ts"}]}