{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-dragonfly",
  "title": "Dragonfly",
  "description": "A four-winged flyer in plan view: fore and hind wings beating half a cycle apart, a solved abdomen that flicks and curls, pointer yaw, and a dart on click. Drawn from any of four camera angles.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/spine-kinematics.json",
    "https://robocn.dev/r/use-pointer-target.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/robot-dragonfly.tsx",
      "content": "\"use client\"\n\n/**\n * robot-dragonfly — a four-winged flyer seen from above.\n *\n * The mechanism is the wing pairs: fore and hind beat half a cycle apart, the\n * way a real dragonfly's do, which is what lets it hold station instead of\n * bobbing through every stroke. A beating wing is foreshortened in plan by the\n * cosine of its own stroke angle rather than redrawn, so the beat is a fact\n * about the geometry from every camera. The abdomen is a `solveSpine` chain\n * that flicks and curls. It yaws toward the pointer, and a click darts.\n */\n\nimport * as React from \"react\"\n\nimport { usePointerTarget } from \"@/hooks/use-pointer-target\"\nimport { useRobotClock } from \"@/hooks/use-robot-motion\"\nimport { clamp, lerp, toRadians, type Vec2 } from \"@/lib/robocn/kinematics\"\nimport { solveSpine } from \"@/lib/robocn/spine\"\nimport {\n  capsulePath,\n  circleFootprint,\n  extrudedPath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  type RobotPaletteProps,\n  type RobotSize,\n  type RobotVariant,\n  type RobotView,\n} from \"@/lib/robocn/style\"\nimport { cn } from \"@/lib/utils\"\n\n/** Seconds a poked dart takes to decay back into whatever it was doing. */\nconst DART = 1.2\n/** Peak stroke angle off the horizontal, in degrees. */\nconst STROKE = 68\n/** How much of a raised joint's height shows as a screen offset in plan view. */\nconst RELIEF = 0.4\n/** Where the wings and the body ride above the ground plane. */\nconst WING_PLANE = 20\nconst BODY_PLANE = 15\n\nexport type DragonflyBehavior = \"hover\" | \"dart\" | \"perch\" | \"static\"\n\n/** The flyer is drawn from straight above; that is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"plan\"\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 RobotDragonflyProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Where the camera stands. One flyer, four projections. */\n  view?: RobotView\n  /** What it does when `phase` is not supplied. */\n  behavior?: DragonflyBehavior\n  /** Controlled wingbeat fraction. Supplying it stops the internal clock. */\n  phase?: number\n  /** Wingbeats per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  /** Seconds of offset, so a swarm breaks step. */\n  offset?: number\n  /** Stroke amplitude, 0 wings held flat to 1 the full beat. Omit and the behavior sets it. */\n  swing?: number\n  /** Abdomen curl out of the wing plane, 0 straight to 1 hooked under. Omit and the behavior sets it. */\n  curl?: number\n  /** Body yaw in degrees, −70..70. Omit and it turns toward the pointer. */\n  heading?: number\n  /** Height above the ground, 0–1. Omit and the behavior decides. */\n  altitude?: number\n  /** Links in the abdomen, 3–24. */\n  segments?: number\n  /** Yaw toward the pointer, and dart when clicked. */\n  interactive?: boolean\n  onDart?: () => void\n  size?: RobotSize | number\n  variant?: RobotVariant\n  /** The ground shadow, and the reed it perches on. */\n  showGround?: boolean\n  label?: string\n}\n\n/** Span and chord of each wing pair, in world units. */\nconst WINGS = [\n  { pair: \"fore\", root: 13, span: 64, chord: 13, offset: 0 },\n  { pair: \"hind\", root: 1, span: 58, chord: 15, offset: 0.5 },\n] as const\n\n/** Abdomen contour length, and the half-width at the thorax. */\nconst ABDOMEN = 86\nconst GIRTH = 5.4\n\nfunction RobotDragonfly({\n  behavior = \"hover\", phase, view = NATIVE_VIEW, speed = 4, animate = true, paused = false, offset = 0,\n  swing, curl, heading, altitude, segments = 9,\n  interactive = true, onDart,\n  size = \"md\", variant = \"solid\", showGround = true, label,\n  color, accent, metal, dark, glow, grid, palette: paletteOverride, className, style,\n  onPointerDown, ...props\n}: RobotDragonflyProps) {\n  const controlled = phase !== undefined\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const clock = useRobotClock({\n    speed: 1,\n    animate: animate && !controlled && behavior !== \"static\",\n    paused,\n    phase: offset,\n  })\n\n  // A poke is one burst: the beat spikes, the body swings, and both decay.\n  const [poked, setPoked] = React.useState<number | null>(null)\n  const since = poked === null ? Infinity : clock - poked\n  const burst = since >= 0 && since < DART ? Math.exp(-since * 3.2) : 0\n\n  const pointer = usePointerTarget(svgRef, {\n    enabled: interactive && heading === undefined && !paused,\n    within: \"element\",\n    persist: true,\n    toWorld: React.useCallback((unit: Vec2) => ({\n      x: clamp((unit.x - 0.5) * 2.2, -1, 1),\n      y: clamp((0.5 - unit.y) * 2, -1, 1),\n    }), []),\n  })\n\n  const scripted = dragonflyBehaviorPose(behavior, clock)\n  const cycle = controlled ? phase : clock * speed * lerp(scripted.rate, 1.7, burst)\n  const beat = Number.isFinite(cycle) ? cycle : 0\n  const depth = finiteClamp(lerp(swing ?? scripted.swing, 1, burst), 0, 1, scripted.swing)\n  const hook = finiteClamp(curl ?? scripted.curl, 0, 1, scripted.curl)\n  const rise = finiteClamp(altitude ?? scripted.altitude, 0, 1, scripted.altitude)\n  const yaw = finiteClamp(\n    heading ?? (pointer.target ? pointer.target.x * 60 : scripted.heading),\n    -70, 70, 0,\n  )\n\n  // The abdomen trails the thorax: a gentle wave with the swing at the tip,\n  // plus a curl that takes the last third of it out of the wing plane.\n  const spine = solveSpine({\n    segments,\n    length: ABDOMEN,\n    phase: beat * 0.25,\n    amplitude: 0.16 * depth + burst * 0.2,\n    waves: 0.6,\n    taper: 0.9,\n    turn: clamp(-yaw / 140, -1, 1),\n  })\n  /** Height of the abdomen at station `s`: the curl lifts the back of it. */\n  const droop = (s: number) => -hook * 30 * s * s\n\n  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })\n  const width = resolveRobotSize(size)\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n\n  const camera = robotCamera(view)\n  const offAxis = view !== NATIVE_VIEW\n  const ground = camera.plane()\n  const state = burst > 0.05 ? \"darting\" : behavior === \"static\" ? \"still\" : behavior === \"hover\" ? \"hovering\" : behavior === \"perch\" ? \"perched\" : behavior\n\n  /** Stroke angle of one wing pair at the current beat, in degrees. */\n  const strokeAngle = (pairOffset: number) =>\n    STROKE * depth * Math.sin(2 * Math.PI * (beat + pairOffset))\n\n  /** Abdomen joint in drawing coordinates: x starboard, y toward the nose. */\n  const bead = (index: number) => {\n    const joint = spine.joints[index]\n    return { x: joint.position.y, y: -8 + joint.position.x, s: joint.s }\n  }\n  /** Abdomen half-width at station `s`: thick at the waist, a point at the tip. */\n  const belly = (s: number) => GIRTH * (0.35 + 0.65 * Math.pow(1 - s, 0.8))\n\n  /** A point in the plan drawing, at `height` above the ground. */\n  const at = (p: { x: number; y: number }, height: number) =>\n    camera.project(p.x, height, -p.y)\n\n  /**\n   * One wing. It is a flat plate in a plane that tilts through the stroke, so\n   * in plan it keeps its chord and loses span by the cosine of the angle —\n   * the projection, not a second drawing.\n   */\n  function wing(side: 1 | -1, index: 0 | 1) {\n    const { pair, root, span, chord, offset: pairOffset } = WINGS[index]\n    const angle = strokeAngle(pairOffset)\n    const foreshorten = Math.cos(toRadians(angle))\n    // The wing also sweeps a little fore and aft through the stroke, which is\n    // what stops the two pairs reading as one rigid cross.\n    const sweep = 9 * Math.cos(2 * Math.PI * (beat + pairOffset)) * depth\n    const reach = span * foreshorten\n    return (\n      <g\n        key={`${pair}-${side}`}\n        data-wing={`${pair}-${side === 1 ? \"right\" : \"left\"}`}\n        transform={`translate(${px(side * 7)} ${root}) rotate(${px(-side * sweep)})`}\n        opacity={px(0.62 + 0.3 * Math.abs(foreshorten))}\n      >\n        <path\n          d={`M 0 ${px(-chord * 0.28)} Q ${px(side * reach * 0.45)} ${px(-chord * 0.6)} ${px(side * reach)} ${px(-chord * 0.16)} Q ${px(side * reach * 0.5)} ${px(chord * 0.5)} 0 ${px(chord * 0.3)} Z`}\n          {...machined}\n          fillOpacity={variant === \"solid\" ? 0.42 : undefined}\n        />\n        {/* Venation: the spars that make the membrane a structure. */}\n        <g stroke={palette.dark} strokeWidth={0.55} opacity={0.55} fill=\"none\">\n          <path d={`M 0 ${px(-chord * 0.26)} L ${px(side * reach * 0.97)} ${px(-chord * 0.16)}`} />\n          {[0.25, 0.48, 0.71].map((t) => (\n            <path\n              key={t}\n              d={`M ${px(side * reach * t)} ${px(-chord * (0.45 - t * 0.28))} L ${px(side * reach * t)} ${px(chord * (0.4 - t * 0.3))}`}\n            />\n          ))}\n        </g>\n        <circle cx={0} cy={0} r={2.4} {...cast} />\n        {/* Pterostigma: the weight near the leading edge of the tip. */}\n        <rect x={px(side * reach * 0.82)} y={px(-chord * 0.32)} width={px(reach * 0.1)} height={2.4} rx={1} fill={palette.accent} opacity={0.85} />\n      </g>\n    )\n  }\n\n  const wingSolid = (side: 1 | -1, index: 0 | 1) => {\n    const { pair, root, span, chord, offset: pairOffset } = WINGS[index]\n    const angle = toRadians(strokeAngle(pairOffset))\n    const hinge = { x: side * 7, y: root }\n    const tip = {\n      x: side * (7 + span * Math.cos(angle)),\n      y: root,\n    }\n    return (\n      <path\n        key={`${pair}-${side}`}\n        data-wing={`${pair}-${side === 1 ? \"right\" : \"left\"}`}\n        d={capsulePath(\n          at(hinge, WING_PLANE + rise * 26),\n          at(tip, WING_PLANE + rise * 26 + span * Math.sin(angle)),\n          px(chord * 0.32),\n        )}\n        {...machined}\n        fillOpacity={variant === \"solid\" ? 0.42 : undefined}\n      />\n    )\n  }\n\n  return (\n    <svg\n      ref={svgRef}\n      role=\"img\"\n      aria-label={`Robot dragonfly, ${state}, ${viewNames[view] ?? viewNames.plan}`}\n      viewBox=\"0 0 240 210\"\n      width={width}\n      height={px(width * 210 / 240)}\n      className={cn(\"max-w-full select-none\", interactive && \"cursor-pointer\", className)}\n      style={{ color: palette.foreground, ...style }}\n      onPointerDown={(event) => {\n        onPointerDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        setPoked(clock)\n        onDart?.()\n      }}\n      {...props}\n    >\n      {variant === \"blueprint\" && (\n        <g fill=\"none\" stroke={palette.grid} strokeWidth={0.5} opacity={0.4}>\n          <path d=\"M 12 104 H 228 M 120 12 V 196\" strokeDasharray=\"2 3\" />\n          <circle cx={120} cy={104} r={71} strokeDasharray=\"2 3\" />\n        </g>\n      )}\n      {showGround && (\n        <g data-ground>\n          <ellipse cx={120} cy={116} rx={px(52 - rise * 20)} ry={px(16 - rise * 6)} fill={palette.dark} opacity={px(0.14 - rise * 0.08)} />\n          {rise < 0.06 && <path d=\"M 120 150 Q 128 176 118 200\" fill=\"none\" stroke={palette.grid} strokeWidth={2.4} strokeLinecap=\"round\" opacity={0.6} />}\n        </g>\n      )}\n\n      {offAxis && <g data-solids transform={`translate(120 ${px(104 - rise * 10)})`}>\n        {([-1, 1] as const).flatMap((side) => [wingSolid(side, 1), wingSolid(side, 0)])}\n        {spine.joints.slice(0, -1).map((_, index) => {\n          const a = bead(index)\n          const b = bead(index + 1)\n          return (\n            <path\n              key={index}\n              data-segment={index}\n              d={capsulePath(\n                at(a, BODY_PLANE + rise * 26 + droop(a.s)),\n                at(b, BODY_PLANE + rise * 26 + droop(b.s)),\n                px(belly(a.s)),\n              )}\n              {...shell}\n            />\n          )\n        })}\n        <path\n          d={extrudedPath(circleFootprint(0, -5, 10, 12), camera, BODY_PLANE + rise * 26 + 7, BODY_PLANE + rise * 26 - 7)}\n          {...shell}\n        />\n        <path\n          d={extrudedPath(circleFootprint(0, -26, 8, 12), camera, BODY_PLANE + rise * 26 + 7, BODY_PLANE + rise * 26 - 6)}\n          {...cast}\n        />\n      </g>}\n\n      <g\n        data-dragonfly\n        data-view={view}\n        transform={`translate(120 ${px(104 - rise * 10)}) ${ground} scale(1 -1) rotate(${px(-yaw)})`.replace(/\\s+/g, \" \")}\n      >\n        <g data-abdomen>\n          {spine.joints.slice(0, -1).map((_, index) => {\n            const a = bead(index)\n            const b = bead(index + 1)\n            return (\n              <g key={index} data-segment={index}>\n                <path\n                  d={capsulePath(\n                    { x: a.x, y: a.y - droop(a.s) * RELIEF },\n                    { x: b.x, y: b.y - droop(b.s) * RELIEF },\n                    px(belly(a.s)),\n                  )}\n                  {...(index % 2 === 0 ? shell : machined)}\n                />\n              </g>\n            )\n          })}\n          {/* Cerci: the two prongs on the tip. */}\n          <path\n            d={`M ${px(bead(spine.joints.length - 1).x - 2)} ${px(bead(spine.joints.length - 1).y - droop(1) * RELIEF - 4)} l -1.5 -5 m 5 5 l 1.5 -5`}\n            fill=\"none\"\n            stroke={palette.dark}\n            strokeWidth={1.6}\n            strokeLinecap=\"round\"\n          />\n        </g>\n\n        <g data-legs stroke={palette.dark} strokeWidth={1.7} strokeLinecap=\"round\" fill=\"none\" opacity={0.8}>\n          {([-1, 1] as const).map((side) =>\n            [0, 1, 2].map((index) => (\n              <path\n                key={`${side}-${index}`}\n                d={`M ${px(side * 6)} ${px(6 - index * 6)} q ${px(side * 8)} ${px(4 + index * 2)} ${px(side * 6)} ${px(13 + index * 3)}`}\n              />\n            )),\n          )}\n        </g>\n\n        <g data-thorax>\n          <path d=\"M -9 -8 Q -11 12 -6 19 L 6 19 Q 11 12 9 -8 Z\" {...shell} />\n          <path d=\"M -7 4 H 7 M -8 10 H 8\" stroke={palette.dark} strokeWidth={0.8} opacity={0.45} fill=\"none\" />\n          <rect x={-4} y={-5} width={8} height={9} rx={2} {...cast} />\n          <circle cx={0} cy={0.5} r={1.8} fill={palette.accent} />\n        </g>\n\n        {([-1, 1] as const).flatMap((side) => [wing(side, 1), wing(side, 0)])}\n\n        <g data-head transform={`translate(0 24) rotate(${px(-yaw * 0.25)})`}>\n          <circle cx={0} cy={0} r={7.5} {...cast} />\n          {/* Compound eyes: nearly the whole head, which is what a dragonfly is. */}\n          {([-1, 1] as const).map((side) => (\n            <g key={side} data-eye={side === 1 ? \"right\" : \"left\"}>\n              <circle cx={px(side * 5.4)} cy={2.4} r={5.6} {...machined} />\n              <circle cx={px(side * 6.2)} cy={3.4} r={2.6} fill={palette.accent} opacity={0.9} />\n            </g>\n          ))}\n          <path d=\"M -2 8 l -1.5 5 M 2 8 l 1.5 5\" stroke={palette.metal} strokeWidth={1.2} strokeLinecap=\"round\" fill=\"none\" />\n        </g>\n\n        {burst > 0.05 && (\n          <g data-wake opacity={px(burst * 0.55)} fill=\"none\" stroke={palette.glow} strokeWidth={1}>\n            {[0, 1, 2].map((index) => (\n              <path key={index} d={`M ${px(-16 - index * 5)} ${px(-40 - index * 10)} q 16 -7 32 0`} />\n            ))}\n          </g>\n        )}\n      </g>\n\n      {label && (\n        <text x={120} y={204} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\" fontSize={6} fill={palette.foreground}>\n          {label}\n        </text>\n      )}\n    </svg>\n  )\n}\n\n/** A control that has to survive a consumer handing it `NaN`. */\nconst finiteClamp = (value: number, min: number, max: number, fallback: number) =>\n  Number.isFinite(value) ? clamp(value, min, max) : fallback\n\n/** What it does with no timeline on it: hold station, burst about, or sit. */\nexport function dragonflyBehaviorPose(behavior: DragonflyBehavior, clock: number) {\n  const time = Number.isFinite(clock) ? clock : 0\n  switch (behavior) {\n    // Burst travel: hard beats and a hard turn, then a coast on slower wings.\n    case \"dart\": {\n      const surge = Math.pow(Math.max(0, Math.sin(time * 0.7)), 3)\n      return {\n        rate: 0.55 + surge * 1.6,\n        swing: 0.5 + 0.5 * surge,\n        curl: 0.1,\n        altitude: 0.6 + 0.3 * surge,\n        heading: 46 * Math.sin(time * 0.7),\n      }\n    }\n    // Perched: wings held out flat, abdomen hooked under, nothing beating.\n    case \"perch\":\n      return { rate: 0, swing: 0, curl: 0.62, altitude: 0, heading: 14 * Math.sin(time * 0.3) }\n    case \"static\":\n      return { rate: 0, swing: 0.55, curl: 0.12, altitude: 0.7, heading: 0 }\n    // Station-keeping: the pairs beat flat out and the body barely moves.\n    default:\n      return {\n        rate: 1,\n        swing: 0.88,\n        curl: 0.14 + 0.06 * Math.sin(time * 0.8),\n        altitude: 0.72 + 0.05 * Math.sin(time * 1.1),\n        heading: 12 * Math.sin(time * 0.4),\n      }\n  }\n}\n\nexport { RobotDragonfly }\n",
      "type": "registry:ui",
      "target": "@ui/robot-dragonfly.tsx"
    }
  ],
  "categories": [
    "robotics",
    "animals"
  ],
  "type": "registry:ui"
}