{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drone-fabricator",
  "title": "Drone fabricator",
  "description": "A free-flying fabricator with no envelope: a repulsor platform that flies to each cell, rides a fixed standoff above the build line, and banks into its own travel while it lays a sampled solid.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/use-robot-motion.json",
    "https://robocn.dev/r/voxel-geometry.json"
  ],
  "files": [
    {
      "path": "src/components/ui/drone-fabricator.tsx",
      "content": "\"use client\"\n\n/**\n * drone-fabricator — the same build, with nothing holding the nozzle up.\n *\n * The gantry runs on rails and the arm reaches from a pedestal; this one has\n * no envelope at all. A repulsor platform flies to the cell being laid, rides\n * a fixed standoff above the build line, and banks into its own travel — so\n * the work volume is bounded by where it can fly rather than by a frame, and\n * a build can start anywhere there is a plate.\n *\n * What is solved: the deposition order, the flight target, and the bank, which\n * is taken from how far the platform still has to travel. What is illustrated:\n * the repulsors — there is no thrust or lift model behind them.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { clamp, toRadians, type Vec2 } from \"@/lib/robocn/kinematics\"\nimport {\n  extrudedPath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  roundedFootprint,\n  type RobotPaletteProps,\n  type RobotSize,\n  type RobotVariant,\n  type RobotView,\n} from \"@/lib/robocn/style\"\nimport {\n  VOXEL_DEFAULT_RESOLUTION,\n  voxelBuild,\n  voxelGoal,\n  voxelLaid,\n  voxelPaths,\n  voxelRefine,\n  voxelResolution,\n  voxelSite,\n  voxelSolid,\n  type VoxelBehavior,\n  type VoxelShape,\n} from \"@/lib/robocn/voxel\"\nimport { cn } from \"@/lib/utils\"\n\n/** Build volume: half-extent in x and z, so the solid runs y 0 to 2 × HALF. */\nconst HALF = 28\nconst PLATE_HALF = 34\nconst PLATE_DROP = 6\n/** How high the platform rides above the cell it is laying. */\nconst RIDE = 26\n/** Where it hovers with nothing left to lay. */\nconst LOITER = { x: 0, y: HALF * 2 + RIDE + 6, z: 0 }\n/** Platform radius, and how far out the repulsor pods sit. */\nconst DECK = 15\nconst POD = 22\nconst BUILD_RATE = 0.6\n/** World units of travel that read as a full bank. */\nconst BANK_SPAN = 26\nconst BANK_LIMIT = 14\n\n/** Screen y of the world origin, chosen so each view fills the frame. */\nconst anchors: Record<RobotView, number> = {\n  plan: 100,\n  front: 141,\n  profile: 141,\n  iso: 138,\n}\n\nconst viewNames: Record<RobotView, string> = {\n  plan: \"plan view\",\n  front: \"front elevation\",\n  profile: \"side elevation\",\n  iso: \"isometric view\",\n}\n\nexport interface DroneFabricatorProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  shape?: VoxelShape\n  /** Voxels along one edge, clamped to 2–14. Animated under `refine`. */\n  resolution?: number\n  /** Controlled build fraction, 0–1. Omit it and the platform runs `behavior`. */\n  progress?: number\n  behavior?: VoxelBehavior\n  /** Build cycles per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** Drag up and down to lay or strip material; arrow keys step a layer. */\n  interactive?: boolean\n  onProgressChange?: (progress: number) => void\n  view?: RobotView\n  /** Repulsor pods around the deck, clamped to 3–6. */\n  pods?: number\n  showPlate?: boolean\n  showGround?: boolean\n  showReadout?: boolean\n  signal?: \"idle\" | \"ready\" | \"warning\"\n  label?: string\n  size?: RobotSize | number\n  variant?: RobotVariant\n}\n\nfunction DroneFabricator({\n  shape = \"vessel\", resolution, progress, behavior = \"build\", speed = 0.12,\n  animate = true, paused = false, phase = 0,\n  interactive = false, onProgressChange,\n  view = \"iso\", pods = 4, showPlate = true, showGround = true, showReadout = true,\n  signal, label, size = \"md\", variant = \"solid\",\n  color, accent, metal, dark, glow, grid, palette: paletteOverride,\n  className, style, role, tabIndex, onKeyDown, onBlur, ...props\n}: DroneFabricatorProps) {\n  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })\n  const width = resolveRobotSize(size)\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const [held, setHeld] = React.useState<number | null>(null)\n\n  const controlled = progress !== undefined\n  const hold = controlled\n    ? (Number.isFinite(progress) ? clamp(progress, 0, 1) : 0)\n    : held\n  const goal = React.useCallback((clock: number) => voxelGoal(behavior, clock), [behavior])\n  const motion = useRobotScalar(goal, {\n    rate: BUILD_RATE,\n    hold,\n    speed,\n    animate: animate && !controlled && behavior !== \"static\",\n    paused,\n    phase,\n  })\n  const built = clamp(motion.value, 0, 1)\n\n  const cells = resolution !== undefined\n    ? voxelResolution(resolution)\n    : behavior === \"refine\"\n      ? voxelResolution(voxelRefine(motion.clock))\n      : VOXEL_DEFAULT_RESOLUTION\n\n  const solid = React.useMemo(() => voxelSolid({ shape, resolution: cells }), [shape, cells])\n  const laid = voxelLaid(solid, built)\n  const part = React.useMemo(() => voxelBuild(solid, laid), [solid, laid])\n\n  const apply = React.useCallback((next: number) => {\n    const bounded = clamp(Number.isFinite(next) ? next : 0, 0, 1)\n    setHeld(bounded)\n    onProgressChange?.(bounded)\n  }, [onProgressChange, setHeld])\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    onDrag: React.useCallback((unit: Vec2) => apply(1 - unit.y), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), [setHeld]),\n  })\n\n  const camera = React.useMemo(() => robotCamera(view), [view])\n  const anchor = anchors[view] ?? anchors.iso\n  const tipped = camera.lift > 0.01\n  const at = (x: number, y: number, z: number) => camera.project(x, y, z)\n  const line = (a: Vec2, b: Vec2) => `M ${px(a.x)} ${px(a.y)} L ${px(b.x)} ${px(b.y)}`\n\n  const cursor = part.active\n  const cell = cursor ? voxelSite(cursor, solid.resolution, HALF) : null\n  // The platform flies to sit over the cell, at a fixed standoff above it.\n  const craft = cell\n    ? { x: cell.x, y: cell.top + RIDE, z: cell.z }\n    : LOITER\n  // The next cell is where it is going; how far it still has to travel is what\n  // the platform banks into. No dynamics behind it — just the lean of a body\n  // that is about to move.\n  const ahead = part.laid + 1 < solid.cells.length\n    ? voxelSite(solid.cells[part.laid + 1], solid.resolution, HALF)\n    : null\n  const bank = ahead\n    ? clamp(((ahead.x - craft.x) / BANK_SPAN) * BANK_LIMIT, -BANK_LIMIT, BANK_LIMIT)\n    : 0\n  const pitch = ahead\n    ? clamp(((ahead.z - craft.z) / BANK_SPAN) * BANK_LIMIT, -BANK_LIMIT, BANK_LIMIT)\n    : 0\n\n  const count = Number.isFinite(pods) ? clamp(Math.round(pods), 3, 6) : 4\n  const arms = Array.from({ length: count }, (_, index) => {\n    const angle = toRadians((index * 360) / count + (count % 2 === 0 ? 45 : 0))\n    const x = Math.sin(angle) * POD\n    const z = -Math.cos(angle) * POD\n    // The deck tilts, so a pod on the low side really does sit lower.\n    const rise = (-x / POD) * bank * 0.35 + (-z / POD) * pitch * 0.35\n    return {\n      id: index,\n      hub: at(craft.x + x, craft.y + rise, craft.z + z),\n      root: at(craft.x + x * 0.35, craft.y, craft.z + z * 0.35),\n      wash: at(craft.x + x, craft.y + rise - 9, craft.z + z),\n      depth: camera.depth(craft.x + x, craft.y + rise, craft.z + z),\n    }\n  })\n  const deckDepth = camera.depth(craft.x, craft.y, craft.z)\n\n  const voxels = React.useMemo(\n    () => voxelPaths(part.surface, solid.resolution, camera, HALF),\n    [part, solid, camera],\n  )\n\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const shade = variant === \"solid\" && tipped\n  const stroke = px(Math.max(0.2, 3 / solid.resolution))\n  const lamp = signal === \"warning\"\n    ? palette.shell\n    : signal === \"idle\"\n      ? palette.metal\n      : signal === \"ready\" || cell\n        ? palette.accent\n        : palette.metal\n\n  const plateFootprint = roundedFootprint(PLATE_HALF, PLATE_HALF, 7, 5)\n  const deckFootprint = roundedFootprint(DECK, DECK, 7, 5).map((point) => ({\n    x: point.x + craft.x, y: point.y + craft.z,\n  }))\n  const hull = extrudedPath(deckFootprint, camera, craft.y + 4, craft.y - 4)\n  const deckFace = extrudedPath(deckFootprint, camera, craft.y + 4, craft.y + 4)\n  const spout = at(craft.x, craft.y - 13, craft.z)\n  const nozzle = extrudedPath([\n    { x: craft.x - 2.6, y: craft.z - 2.6 }, { x: craft.x + 2.6, y: craft.z - 2.6 },\n    { x: craft.x + 2.6, y: craft.z + 2.6 }, { x: craft.x - 2.6, y: craft.z + 2.6 },\n  ], camera, craft.y - 4, craft.y - 13)\n  const target = cell ? at(cell.x, cell.top, cell.z) : spout\n  const drift = at(craft.x, -PLATE_DROP, craft.z)\n  const groundAt = at(0, -PLATE_DROP, 0)\n\n  const percent = Math.round(built * 100)\n\n  const pod = ({ id, hub, root, wash }: (typeof arms)[number]) => (\n    <g key={id} data-pod={id}>\n      <path d={line(root, hub)} stroke={palette.dark} strokeWidth={6} strokeLinecap=\"round\" />\n      <path d={line(root, hub)} stroke={palette.metal} strokeWidth={3.5} strokeLinecap=\"round\" />\n      <circle cx={px(hub.x)} cy={px(hub.y)} r={5} fill={palette.dark} />\n      <circle cx={px(hub.x)} cy={px(hub.y)} r={2.6} fill={lamp} />\n      {variant !== \"wire\" && (\n        <path data-wash d={line(hub, wash)} stroke={palette.glow} strokeWidth={7}\n          strokeLinecap=\"round\" opacity={0.16} />\n      )}\n    </g>\n  )\n\n  return (\n    <svg ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={`Drone fabricator building a ${solid.shape}, ${solid.resolution} voxels per edge, ${percent} per cent complete, ${viewNames[view] ?? viewNames.iso}`}\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 100 : undefined}\n      aria-valuenow={interactive ? percent : undefined}\n      aria-valuetext={interactive ? `${percent} per cent, layer ${Math.min(part.layer + 1, solid.layers)} of ${solid.layers}` : undefined}\n      tabIndex={tabIndex ?? (interactive ? 0 : undefined)}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        const layer = 1 / Math.max(1, solid.layers)\n        const delta = arrowStep(event.key, event.shiftKey ? layer * 3 : layer, layer * 5)\n        if (delta !== 0) apply(built + delta)\n        else if (event.key === \"Home\") apply(0)\n        else if (event.key === \"End\") apply(1)\n        else return\n        event.preventDefault()\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        if (!dragging) setHeld(null)\n      }}\n      viewBox=\"0 0 200 200\" width={width} height={width}\n      className={cn(\"max-w-full select-none\", interactive && \"cursor-grab touch-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[currentColor]\", dragging && \"cursor-grabbing\", className)}\n      style={{ color: palette.foreground, ...style }} {...props}>\n\n      <g data-cell data-view={view} transform={`translate(100 ${px(anchor)})`}>\n        {showGround && variant !== \"wire\" && (\n          <>\n            <ellipse data-shadow cx={px(groundAt.x)} cy={px(groundAt.y + 4)}\n              rx={PLATE_HALF + 4} ry={px((PLATE_HALF + 4) * Math.max(0.12, camera.flatten))}\n              fill={palette.dark} opacity={0.12} />\n            {/* The platform's own shadow, tracking it across the plate. */}\n            <ellipse data-craft-shadow cx={px(drift.x)} cy={px(drift.y + 4)}\n              rx={DECK} ry={px(DECK * Math.max(0.1, camera.flatten))}\n              fill={palette.dark} opacity={0.1} />\n          </>\n        )}\n\n        {variant === \"blueprint\" && (\n          <g data-annotation fill=\"none\" stroke={palette.grid} strokeWidth={0.5} strokeDasharray=\"2 3\" opacity={0.55}>\n            {[0, HALF * 2].map((y) => (\n              <path key={y} d={outline([\n                at(-HALF, y, -HALF), at(HALF, y, -HALF), at(HALF, y, HALF), at(-HALF, y, HALF),\n              ])} />\n            ))}\n          </g>\n        )}\n\n        {showPlate && (\n          <g data-plate>\n            <path d={extrudedPath(plateFootprint, camera, 0, -PLATE_DROP)} {...cast} />\n            <path d={outline(plateFootprint.map((point) => camera.project(point.x, 0, point.y)))} {...machined} />\n          </g>\n        )}\n\n        <g data-workpiece>\n          {voxels.map((voxel) => (\n            <React.Fragment key={voxel.id}>\n              <path data-voxel data-layer={voxel.layer} d={voxel.hull} {...shell} strokeWidth={stroke} />\n              {shade && <path d={voxel.hull} fill={palette.dark} opacity={0.2} />}\n              {voxel.cap && <path data-face=\"top\" d={voxel.cap} {...shell} strokeWidth={stroke} />}\n            </React.Fragment>\n          ))}\n        </g>\n\n        {cell && variant !== \"wire\" && (\n          <g data-beam>\n            <path d={line(spout, target)} stroke={palette.glow} strokeWidth={4} strokeLinecap=\"round\" opacity={0.25} />\n            <path d={line(spout, target)} stroke={palette.accent} strokeWidth={1.4} strokeLinecap=\"round\" />\n          </g>\n        )}\n\n        <g data-craft>\n          {arms.filter((arm) => arm.depth <= deckDepth).map(pod)}\n          <path data-nozzle d={nozzle} {...cast} />\n          <path d={hull} {...shell} />\n          {tipped && variant === \"solid\" && (\n            <>\n              <path d={hull} fill={palette.dark} opacity={0.18} />\n              <path d={deckFace} {...shell} />\n            </>\n          )}\n          <g data-deck transform={`translate(${px(at(craft.x, craft.y + 4, craft.z).x)} ${px(at(craft.x, craft.y + 4, craft.z).y)})`}>\n            <g transform={camera.plane()}>\n              <rect x={-7} y={-9} width={14} height={18} rx={3} {...cast} />\n              <rect x={-5} y={-6} width={10} height={5} rx={1.5} {...machined} />\n              <circle cy={5} r={2.4} fill={lamp} />\n            </g>\n          </g>\n          {arms.filter((arm) => arm.depth > deckDepth).map(pod)}\n        </g>\n      </g>\n\n      {showReadout && (\n        <text data-readout x={100} y={191} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\"\n          fontSize={5.5} fill={palette.foreground}>{`${percent}% · ${solid.resolution}³`}</text>\n      )}\n      {label && (\n        <text x={100} y={198} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\"\n          fontSize={4.5} fill={palette.foreground} opacity={0.75}>{label}</text>\n      )}\n    </svg>\n  )\n}\n\nconst outline = (points: Vec2[]) =>\n  `${points.map((point, index) => `${index ? \"L\" : \"M\"} ${px(point.x)} ${px(point.y)}`).join(\" \")} Z`\n\nexport { DroneFabricator }\n",
      "type": "registry:ui",
      "target": "@ui/drone-fabricator.tsx"
    }
  ],
  "categories": [
    "robotics",
    "fabrication"
  ],
  "type": "registry:ui"
}