{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-strawberry",
  "title": "Strawberry",
  "description": "A berry-shelled field unit whose sensor studs are placed by the golden angle over equal areas of its own skin and run out along their own surface normals, under a calyx of rigid blades on one hinge. Four camera angles, and a drag to work the collar.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/produce-geometry.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/robot-strawberry.tsx",
      "content": "\"use client\"\n\n/**\n * robot-strawberry — a berry-shelled field unit with a lattice of sensor studs.\n *\n * The mechanism is the skin. The studs are placed by the golden angle over the\n * *surface area* of the profile rather than over its parameter, so they sit an\n * equal distance apart on a body whose radius changes all the way up, and each\n * one runs out along its own surface normal. Above them the calyx is a ring of\n * rigid blades on one hinge: `bloom` opens them and drives the studs out\n * together.\n *\n * One geometry, four cameras; the studs and blades are hidden-line culled\n * against the same surface they sit on. Design note: docs/produce-robots.md.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { clamp, convexHull2, type Vec2, type Vec3 } from \"@/lib/robocn/kinematics\"\nimport {\n  bladeRing,\n  goldenLattice,\n  latitudeRing,\n  revolveProfile,\n  type ProduceProfile,\n} from \"@/lib/robocn/produce\"\nimport {\n  aboutPoint,\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\nconst VIEW_WIDTH = 190\nconst VIEW_HEIGHT = 172\n/** The centre of the foot cup, on the ground, in view units. */\nconst ORIGIN = { x: 95, y: 142 }\nconst NATIVE_VIEW: RobotView = \"front\"\n\nconst FLOOR = 6\nconst BODY_HEIGHT = 84\nconst SHOULDER_RADIUS = 31\n/** The profile's own peak, so the shoulder radius means what it says. */\nconst PROFILE_PEAK = 0.4726\n/** Where the studs stop: the tip pad below, the calyx ring above. */\nconst LATTICE_FROM = 0.08\nconst LATTICE_TO = 0.9\n/** How far a stud stands off the skin, seated and run out. */\nconst STUD_SEATED = 1.1\nconst STUD_TRAVEL = 3.6\nconst STUD_RADIUS = 1.6\n/** The calyx: where it is hinged, and the arc its blades work through. */\nconst CALYX_T = 0.9\nconst BLADE_LENGTH = 19\nconst BLADE_WIDTH = 19\nconst BLADE_FURLED = 54\nconst BLADE_SPLAYED = -58\n/** Bloom per second while the calyx is returning to its behaviour. */\nconst BLOOM_RATE = 0.9\nconst RINGS = 12\nconst MERIDIANS = 24\n\nconst viewNames: Record<RobotView, string> = {\n  plan: \"plan view\",\n  front: \"front elevation\",\n  profile: \"side elevation\",\n  iso: \"isometric view\",\n}\n\nconst framing: Record<RobotView, { zoom: number; rise: number }> = {\n  plan: { zoom: 0.9, rise: -48 },\n  front: { zoom: 1, rise: 0 },\n  profile: { zoom: 1, rise: 0 },\n  iso: { zoom: 0.95, rise: -6 },\n}\n\n/**\n * The body: a point at the floor swelling to a shoulder three quarters of the\n * way up and doming over into the calyx seat, so the surface area — and with\n * it the lattice — crowds toward the top.\n */\nexport const strawberryProfile: ProduceProfile = (t) => {\n  const station = clamp(t, 0, 1)\n  return {\n    height: FLOOR + BODY_HEIGHT * station,\n    radius:\n      (SHOULDER_RADIUS * Math.pow(station, 0.85) * Math.sqrt(Math.max(0, 1 - 0.85 * station))) /\n      PROFILE_PEAK,\n  }\n}\n\nexport type StrawberryBehavior = \"unfurl\" | \"probe\" | \"furl\" | \"static\"\n\nexport interface RobotStrawberryProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  size?: RobotSize | number\n  variant?: RobotVariant\n  /** Where the camera stands. One berry, four projections. */\n  view?: RobotView\n  /** Controlled bloom, 0 furled and seated to 1 splayed and run out. */\n  bloom?: number\n  behavior?: StrawberryBehavior\n  /** Cycles per second: one open-and-furl. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** Drag up and down to work the calyx, or arrow-key it. */\n  interactive?: boolean\n  onBloomChange?: (bloom: number) => void\n  /** Studs on the skin, clamped to 10..48. */\n  seeds?: number\n  /** Calyx blades, clamped to 3..9. */\n  blades?: number\n  signal?: \"idle\" | \"ready\" | \"warning\"\n  showGround?: boolean\n  label?: string\n}\n\nfunction RobotStrawberry({\n  size = \"md\",\n  variant = \"solid\",\n  view = NATIVE_VIEW,\n  bloom,\n  behavior = \"unfurl\",\n  speed = 0.26,\n  animate = true,\n  paused = false,\n  phase = 0,\n  interactive = false,\n  onBloomChange,\n  seeds = 26,\n  blades = 6,\n  signal = \"ready\",\n  showGround = true,\n  label,\n  color,\n  accent,\n  metal,\n  dark,\n  glow,\n  grid,\n  palette: paletteOverride,\n  className,\n  style,\n  role,\n  tabIndex,\n  onKeyDown,\n  onBlur,\n  ...props\n}: RobotStrawberryProps) {\n  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })\n  const width = resolveRobotSize(size)\n  const controlled = bloom !== undefined\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const [held, setHeld] = React.useState<number | null>(null)\n\n  const hold = controlled ? clamp(Number.isFinite(bloom) ? bloom : 0, 0, 1) : held\n  const goal = React.useCallback((clock: number) => strawberryGoal(behavior, clock), [behavior])\n  const motion = useRobotScalar(goal, {\n    rate: BLOOM_RATE,\n    hold,\n    speed,\n    animate: animate && !controlled && behavior !== \"static\",\n    paused,\n    phase,\n  })\n\n  const open = clamp(motion.value, 0, 1)\n  const percent = Math.round(open * 100)\n  const studCount = Math.round(clamp(Number.isFinite(seeds) ? seeds : 26, 10, 48))\n  const bladeCount = Math.round(clamp(Number.isFinite(blades) ? blades : 6, 3, 9))\n\n  const apply = React.useCallback(\n    (next: number) => {\n      const bounded = clamp(Number.isFinite(next) ? next : 0, 0, 1)\n      setHeld(bounded)\n      onBloomChange?.(bounded)\n    },\n    [onBloomChange],\n  )\n\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    // Pull down and the calyx comes down with the pointer.\n    onDrag: React.useCallback((unit: Vec2) => apply(unit.y * 1.3 - 0.15), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), []),\n  })\n\n  const camera = robotCamera(view)\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const signalColor =\n    signal === \"warning\" ? palette.shell : signal === \"ready\" ? palette.accent : palette.metal\n\n  /** Produce space puts azimuth 0 at +z; this machine faces the camera at −z. */\n  const world = (point: Vec3): Vec3 => ({ x: point.x, y: point.y, z: -point.z })\n  const at = (point: Vec3): Vec2 => {\n    const screen = camera.project(point.x, point.y, point.z)\n    return { x: ORIGIN.x + screen.x, y: ORIGIN.y + screen.y }\n  }\n  const towardCamera = (point: Vec3) => camera.depth(point.x, point.y, point.z)\n  const panelAt = (n: Vec3, p: Vec3) => {\n    const across = camera.project(n.z, 0, -n.x)\n    const centre = at(p)\n    return `matrix(${px(across.x)} ${px(across.y)} 0 ${px(camera.lift)} ${px(centre.x)} ${px(centre.y)})`\n  }\n\n  const bodyPath = hullPath(\n    revolveProfile(strawberryProfile, { rings: RINGS, meridians: MERIDIANS })\n      .map(world)\n      .map(at),\n  )\n  const shoulder = linePath(\n    latitudeRing(strawberryProfile, 0.72, { meridians: 40 }, 40)\n      .map(world)\n      .filter((point) => towardCamera(point) > 0)\n      .map(at),\n  )\n\n  const reach = STUD_SEATED + STUD_TRAVEL * open\n  const studs = goldenLattice(strawberryProfile, studCount, {\n    from: LATTICE_FROM,\n    to: LATTICE_TO,\n  }).map((site) => {\n    const seat = world(site.position)\n    const normal = world(site.normal)\n    const tip = {\n      x: seat.x + normal.x * reach,\n      y: seat.y + normal.y * reach,\n      z: seat.z + normal.z * reach,\n    }\n    return {\n      index: site.index,\n      normal,\n      seat,\n      facing: towardCamera(normal) > 0.08,\n      pit: panelAt(normal, seat),\n      stud: capsulePath(at(seat), at(tip), STUD_RADIUS),\n    }\n  })\n\n  const calyx = bladeRing(bladeCount, {\n    radius: strawberryProfile(CALYX_T).radius * 0.94,\n    height: strawberryProfile(CALYX_T).height,\n    length: BLADE_LENGTH,\n    width: BLADE_WIDTH,\n    taper: 0.22,\n    pitch: BLADE_FURLED + (BLADE_SPLAYED - BLADE_FURLED) * open,\n  }).map((blade) => {\n    const corners = blade.corners.map(world)\n    const root = world(blade.root)\n    const tip = world(blade.tip)\n    return {\n      index: blade.index,\n      depth: towardCamera({ x: (root.x + tip.x) / 2, y: (root.y + tip.y) / 2, z: (root.z + tip.z) / 2 }),\n      path: polygonPath(corners.map(at)),\n      rib: linePath([at(root), at(tip)]),\n    }\n  })\n  calyx.sort((a, b) => a.depth - b.depth)\n  const bodyDepth = towardCamera({ x: 0, y: strawberryProfile(CALYX_T).height, z: 0 })\n\n  const crown = strawberryProfile(1)\n  const stemFoot = { x: 0, y: crown.height - 2, z: 0 }\n  const stemHead = { x: 0, y: crown.height + 8, z: 0 }\n  const foot = extrudedPath(circleFootprint(0, 0, 9.5, 14), camera, FLOOR + 1.5, 0)\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={`Robot strawberry, calyx ${percent} percent open, ${viewNames[view] ?? viewNames.front}`}\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 100 : undefined}\n      aria-valuenow={interactive ? percent : undefined}\n      aria-valuetext={interactive ? `${percent} percent open` : undefined}\n      tabIndex={tabIndex ?? (interactive ? 0 : undefined)}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        const delta = arrowStep(event.key, event.shiftKey ? 0.15 : 0.05, 0.25)\n        if (delta !== 0) apply(open + 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 ${VIEW_WIDTH} ${VIEW_HEIGHT}`}\n      width={width}\n      height={px((width * VIEW_HEIGHT) / VIEW_WIDTH)}\n      className={cn(\n        \"max-w-full select-none\",\n        interactive &&\n          \"cursor-grab touch-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[currentColor]\",\n        dragging && \"cursor-grabbing\",\n        className,\n      )}\n      style={{ color: palette.foreground, ...style }}\n      {...props}\n    >\n      {variant === \"blueprint\" && (\n        <g fill=\"none\" stroke={palette.grid} strokeWidth={0.5} opacity={0.45}>\n          <path\n            d={`M 12 ${ORIGIN.y} H 178 M ${ORIGIN.x} 14 V ${ORIGIN.y + 12}`}\n            strokeDasharray=\"2 3\"\n          />\n        </g>\n      )}\n\n      <g\n        data-frame\n        data-view={view}\n        transform={aboutPoint(\n          framing[view]?.rise ? `translate(0 ${framing[view].rise})` : \"\",\n          ORIGIN.x,\n          ORIGIN.y,\n          framing[view]?.zoom ?? 1,\n        )}\n      >\n        {showGround && (\n          <ellipse\n            cx={ORIGIN.x}\n            cy={ORIGIN.y}\n            rx={px(SHOULDER_RADIUS * 0.9)}\n            ry={px(Math.max(2.4, SHOULDER_RADIUS * 0.9 * camera.flatten))}\n            fill={palette.dark}\n            opacity={0.14}\n          />\n        )}\n\n        <g data-foot transform={`translate(${ORIGIN.x} ${ORIGIN.y})`}>\n          <path d={foot} {...machined} />\n        </g>\n\n        {calyx\n          .filter((blade) => blade.depth <= bodyDepth)\n          .map((blade) => (\n            <g key={`far-${blade.index}`}>\n              <path data-blade={blade.index} d={blade.path} {...machined} />\n              <path d={blade.rib} fill=\"none\" stroke={palette.dark} strokeWidth={0.7} opacity={0.5} />\n            </g>\n          ))}\n\n        <path data-body d={bodyPath} {...shell} />\n        <path d={shoulder} fill=\"none\" stroke={palette.dark} strokeWidth={0.8} opacity={0.18} />\n\n        {studs\n          .filter((stud) => stud.facing)\n          .map((stud) => (\n            <g key={stud.index}>\n              <ellipse transform={stud.pit} rx={2.4} ry={1.6} fill={palette.dark} opacity={0.4} />\n              <path data-seed={stud.index} d={stud.stud} {...machined} />\n              <circle\n                cx={px(at({\n                  x: stud.seat.x + stud.normal.x * reach,\n                  y: stud.seat.y + stud.normal.y * reach,\n                  z: stud.seat.z + stud.normal.z * reach,\n                }).x)}\n                cy={px(at({\n                  x: stud.seat.x + stud.normal.x * reach,\n                  y: stud.seat.y + stud.normal.y * reach,\n                  z: stud.seat.z + stud.normal.z * reach,\n                }).y)}\n                r={0.8}\n                fill={palette.dark}\n                opacity={0.55}\n              />\n            </g>\n          ))}\n\n        {calyx\n          .filter((blade) => blade.depth > bodyDepth)\n          .map((blade) => (\n            <g key={`near-${blade.index}`}>\n              <path data-blade={blade.index} d={blade.path} {...machined} />\n              <path d={blade.rib} fill=\"none\" stroke={palette.dark} strokeWidth={0.7} opacity={0.5} />\n            </g>\n          ))}\n\n        <path data-stem d={capsulePath(at(stemFoot), at(stemHead), 2.3)} {...cast} />\n        <circle\n          data-lamp\n          cx={px(at(stemHead).x)}\n          cy={px(at(stemHead).y)}\n          r={2.4}\n          fill={signalColor}\n          className={signal === \"ready\" ? \"robocn-pulse\" : undefined}\n        />\n      </g>\n\n      {variant === \"blueprint\" && (\n        <text x={178} y={24} textAnchor=\"end\" fontFamily=\"ui-monospace, monospace\" fontSize={5} fill={palette.grid}>\n          {percent}%\n        </text>\n      )}\n      {label && (\n        <text x={ORIGIN.x} y={166} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\" fontSize={6} fill={palette.foreground}>\n          {label}\n        </text>\n      )}\n    </svg>\n  )\n}\n\n/** How far open the calyx is aiming to be at `clock`, 0..1. */\nexport function strawberryGoal(behavior: StrawberryBehavior, clock: number): number {\n  if (behavior === \"static\") return 0.45\n  const t = Number.isFinite(clock) ? ((clock % 1) + 1) % 1 : 0\n  switch (behavior) {\n    // Open, work, furl again.\n    case \"unfurl\":\n      if (t < 0.28) return (t / 0.28) * 0.92\n      if (t < 0.7) return 0.92\n      return 0.92 * (1 - (t - 0.7) / 0.3)\n    // Shut, bar the two moments it cracks open to look.\n    case \"furl\":\n      return 0.06 + 0.46 * Math.max(crack(t, 0.2), crack(t, 0.66))\n    // Working the middle of the range, studs never fully seated.\n    default:\n      return 0.5 + 0.18 * Math.sin(t * Math.PI * 2)\n  }\n}\n\n/** A short spike either side of `at`, on a cycle that wraps. */\nfunction crack(t: number, at: number) {\n  const gap = Math.abs(t - at)\n  return Math.max(0, 1 - Math.min(gap, 1 - gap) / 0.1)\n}\n\n/** The outline round a set of projected points: any solid, from any angle. */\nfunction hullPath(points: readonly Vec2[]): string {\n  const hull = convexHull2(points)\n  if (hull.length < 3) return \"\"\n  return `${hull.map((p, i) => `${i ? \"L\" : \"M\"} ${px(p.x)} ${px(p.y)}`).join(\" \")} Z`\n}\n\n/** A planar polygon, in the order it was built: a blade, a flat panel. */\nfunction polygonPath(points: readonly Vec2[]): string {\n  if (points.length < 3) return \"\"\n  return `${points.map((p, i) => `${i ? \"L\" : \"M\"} ${px(p.x)} ${px(p.y)}`).join(\" \")} Z`\n}\n\n/** An open polyline: a seam, a rib, a latitude. */\nfunction linePath(points: readonly Vec2[]): string {\n  if (points.length < 2) return \"\"\n  return points.map((p, i) => `${i ? \"L\" : \"M\"} ${px(p.x)} ${px(p.y)}`).join(\" \")\n}\n\nexport { RobotStrawberry }\n",
      "type": "registry:ui",
      "target": "@ui/robot-strawberry.tsx"
    }
  ],
  "categories": [
    "robotics",
    "garden"
  ],
  "type": "registry:ui"
}