{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "robot-hand",
  "title": "Hand",
  "description": "A five-digit hand on a real saddle-jointed thumb: fingers that abduct about the palm normal, seven named grasps, a handedness, an articulated wrist, and the pad gap a pinch closes. Drawn from any of four camera angles.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/hand-kinematics.json",
    "https://robocn.dev/r/use-robot-motion.json",
    "https://robocn.dev/r/robot-kinematics.json"
  ],
  "files": [
    {
      "path": "src/components/ui/robot-hand.tsx",
      "content": "\"use client\"\n\n/**\n * robot-hand — five digits, a saddle-jointed thumb, and a wrist.\n *\n * The whole pose comes out of `hand-kinematics`: four fingers as three-link\n * chains posed forward in planes abducted about the palm normal, and a thumb\n * whose carpometacarpal joint really is a saddle — swung across the palm,\n * lifted out of it, and rolled about its own metacarpal, which is what turns\n * the pad to face the fingers instead of facing forward.\n *\n * That is why `pinch` closes on something here: the gap between the thumb pad\n * and the index pad is a distance the solver produces, drawn as a caliper and\n * reported in the readout. It is a shape, not a grip — the hand is not holding\n * anything and does not claim to be.\n *\n * One model, four cameras. Hand-local axes are `x` toward the thumb, `y` up\n * the hand, `z` out of the palm; the palm faces the `front` camera.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport {\n  handGoal,\n  handGrasps,\n  handWave,\n  solveHand,\n  type HandBehavior,\n  type HandDigit,\n  type HandGrasp,\n  type HandSide,\n} from \"@/lib/robocn/hand\"\nimport { clamp, type Vec2, type Vec3 } from \"@/lib/robocn/kinematics\"\nimport {\n  capsulePath,\n  circleFootprint,\n  extrudedPath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  slabPath,\n  type RobotPaletteProps,\n  type RobotSize,\n  type RobotVariant,\n  type RobotView,\n} from \"@/lib/robocn/style\"\nimport { cn } from \"@/lib/utils\"\n\nexport type { HandBehavior, HandGrasp, HandSide }\n\nconst VIEW_WIDTH = 170\nconst VIEW_HEIGHT = 190\n/** World origin on screen: the wrist, where the hand starts. */\nconst CENTRE = { x: 85, y: 126 }\n/** Curl (0–1) per second while easing back into the behaviour. */\nconst SLEW_RATE = 1.4\nconst CLICK_SLOP = 0.04\n/** Palm on, which is the camera it defaults to. */\nconst NATIVE_VIEW: RobotView = \"front\"\n/** A pinch reads as closed once the pads are this near, in world units. */\nconst PINCH_CLOSE = 14\n\nconst fits: Record<RobotView, number> = {\n  plan: 0.9,\n  front: 0.86,\n  profile: 0.86,\n  iso: 0.8,\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 RobotHandProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Controlled closure, 0 open to 1 shut, scaling the grasp. Omit to run `behavior`. */\n  curl?: number\n  /** Which grip the digits close into. */\n  grasp?: HandGrasp\n  /** Per-digit closure, thumb first, overriding the grasp digit by digit. */\n  digits?: readonly (number | null | undefined)[]\n  /** Finger fan, -1 pressed together to 1 splayed. Defaults to the grasp's. */\n  spread?: number\n  /** Thumb across the palm, 0 alongside to 1 opposed. Defaults to the grasp's. */\n  opposition?: number\n  /** A hand is handed. `left` is `right` mirrored, not a second drawing. */\n  side?: HandSide\n  /** Wrist flexion in degrees, positive toward the palm. */\n  wristPitch?: number\n  /** Wrist deviation in degrees, positive toward the thumb. */\n  wristYaw?: number\n  /** What the hand does when `curl` is not supplied. */\n  behavior?: HandBehavior\n  /** Where the camera stands. One hand, four projections. */\n  view?: RobotView\n  /** Grips per second, or waves per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** Drag to close the hand; click steps to the next grasp. */\n  interactive?: boolean\n  onCurlChange?: (curl: number) => void\n  onGraspChange?: (grasp: HandGrasp) => void\n  showWrist?: boolean\n  /** Draw the caliper between the thumb pad and the index pad. */\n  showPinch?: boolean\n  label?: string\n  size?: RobotSize | number\n  variant?: RobotVariant\n}\n\nfunction RobotHand({\n  curl,\n  grasp = \"power\",\n  digits,\n  spread,\n  opposition,\n  side = \"right\",\n  wristPitch = 0,\n  wristYaw = 0,\n  behavior = \"grip\",\n  view = NATIVE_VIEW,\n  speed = 0.3,\n  animate = true,\n  paused = false,\n  phase = 0,\n  interactive = false,\n  onCurlChange,\n  onGraspChange,\n  showWrist = true,\n  showPinch = true,\n  label,\n  size = \"md\",\n  variant = \"solid\",\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}: RobotHandProps) {\n  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })\n  const width = resolveRobotSize(size)\n  const controlled = curl !== undefined\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const [held, setHeld] = React.useState<number | null>(null)\n  const [picked, setPicked] = React.useState<HandGrasp | null>(null)\n  const shown = picked ?? (handGrasps.includes(grasp) ? grasp : \"power\")\n\n  const hold = controlled ? (Number.isFinite(curl) ? clamp(curl, 0, 1) : 0) : held\n  const goal = React.useCallback((clock: number) => handGoal(behavior, clock), [behavior])\n  const motion = useRobotScalar(goal, {\n    rate: Math.max(SLEW_RATE, Math.abs(speed) * 5),\n    hold,\n    speed,\n    animate: animate && !controlled && behavior !== \"static\",\n    paused,\n    phase,\n  })\n  const master = clamp(Number.isFinite(motion.value) ? motion.value : 0, 0, 1)\n\n  const apply = React.useCallback(\n    (next: number) => {\n      const bounded = clamp(next, 0, 1)\n      setHeld(bounded)\n      onCurlChange?.(bounded)\n    },\n    [onCurlChange, setHeld],\n  )\n  const step = React.useCallback(() => {\n    const next = handGrasps[(handGrasps.indexOf(shown) + 1) % handGrasps.length]\n    setPicked(next)\n    onGraspChange?.(next)\n  }, [shown, onGraspChange, setPicked])\n\n  const press = React.useRef<{ from: number; at: number; moved: boolean } | null>(null)\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    onDrag: React.useCallback(\n      (unit: Vec2) => {\n        if (!press.current) {\n          press.current = { from: master, at: unit.y, moved: false }\n          return\n        }\n        const moved = unit.y - press.current.at\n        if (Math.abs(moved) > CLICK_SLOP) press.current.moved = true\n        if (press.current.moved) apply(press.current.from + moved * 2.2)\n      },\n      [apply, master],\n    ),\n    onDragEnd: React.useCallback(() => {\n      const started = press.current\n      press.current = null\n      if (started && !started.moved) step()\n    }, [step]),\n  })\n\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n\n  // The ripple is the one behaviour a single scalar cannot carry: each digit\n  // needs its own phase, so it reads the clock directly.\n  const rippling = !controlled && held === null && behavior === \"wave\"\n  const rippled = rippling\n    ? Array.from({ length: 5 }, (_, i) => handWave(motion.clock, i))\n    : undefined\n\n  const pose = solveHand({\n    grasp: shown,\n    curl: master,\n    digits: rippled ?? digits,\n    spread,\n    opposition,\n    side,\n    wristPitch,\n    wristYaw,\n  })\n\n  const camera = robotCamera(view)\n  const fit = fits[view] ?? 1\n  /** Hand-local to world: the palm normal points at the `front` camera. */\n  const world = (point: Vec3): Vec3 => ({ x: point.x, y: point.y, z: -point.z })\n  const to = (point: Vec3): Vec2 => {\n    const w = world(point)\n    return camera.project(w.x, w.y, w.z)\n  }\n  const depthOf = (point: Vec3) => {\n    const w = world(point)\n    return camera.depth(w.x, w.y, w.z)\n  }\n\n  /** An outline in the hand plane given a thickness out of the palm. */\n  const slab = (outline: Vec2[], front: number, back: number) =>\n    slabPath(\n      outline.flatMap((point) => [\n        world({ x: point.x, y: point.y, z: front }),\n        world({ x: point.x, y: point.y, z: back }),\n      ]),\n      camera,\n    )\n\n  const line = (a: Vec3, b: Vec3) => {\n    const from = to(a)\n    const at = to(b)\n    return `M ${px(from.x)} ${px(from.y)} L ${px(at.x)} ${px(at.y)}`\n  }\n\n  const readout = Math.round(master * 100)\n  const gap = pose.pinch.gap\n  const closed = clamp(1 - gap / PINCH_CLOSE, 0, 1)\n\n  const digitPart = (item: HandDigit) => {\n    const points = item.joints.map(to)\n    return (\n      <g key={item.name} data-digit={item.name}>\n        {points.slice(0, -1).map((joint, i) => (\n          <g key={i} data-phalanx={`${item.name}-${i}`}>\n            <path\n              d={capsulePath(joint, points[i + 1], item.radii[i] ?? 3.4)}\n              {...(i % 2 ? machined : shell)}\n            />\n            <circle\n              cx={px(joint.x)}\n              cy={px(joint.y)}\n              r={px((item.radii[i] ?? 3.4) * 0.52)}\n              {...cast}\n            />\n          </g>\n        ))}\n        <circle\n          data-pad={item.name}\n          cx={px(to(item.pad).x)}\n          cy={px(to(item.pad).y)}\n          r={px((item.radii[item.radii.length - 1] ?? 3) * 0.7)}\n          fill={palette.accent}\n          fillOpacity={0.35 + item.closure * 0.65}\n        />\n      </g>\n    )\n  }\n\n  // Draw back to front, so a curled digit passes in front of the palm the way\n  // the camera says it should rather than the way the array happens to be.\n  const ordered = [...pose.digits].sort(\n    (a, b) => depthOf(a.joints[1]) - depthOf(b.joints[1]),\n  )\n  const behind = ordered.filter((item) => depthOf(item.joints[1]) < 0)\n  const infront = ordered.filter((item) => depthOf(item.joints[1]) >= 0)\n\n  const palmFace = (\n    <g data-palm>\n      <path d={slab(pose.palm, pose.palmFront, pose.palmBack)} {...shell} />\n      {/* Thenar plate: the muscle pad the thumb's saddle joint sits under. */}\n      <path\n        d={slab(\n          pose.palm.map((point) => ({\n            x: point.x * 0.5 + (side === \"left\" ? -9 : 9),\n            y: point.y * 0.48 + 12,\n          })),\n          pose.palmFront + 1.8,\n          pose.palmFront,\n        )}\n        {...machined}\n      />\n      {/* Tendon runs from the wrist collar up to each knuckle. */}\n      <g fill=\"none\" stroke={palette.dark} strokeWidth={0.9} opacity={0.55}>\n        {pose.digits.slice(1).map((item) => (\n          <path\n            key={item.name}\n            d={line(\n              { x: item.joints[0].x * 0.35, y: 6, z: pose.palmFront },\n              { x: item.joints[0].x, y: item.joints[0].y - 6, z: pose.palmFront },\n            )}\n          />\n        ))}\n      </g>\n      {pose.digits.slice(1).map((item) => {\n        const knuckle = to(item.joints[0])\n        return (\n          <circle\n            key={item.name}\n            data-knuckle={item.name}\n            cx={px(knuckle.x)}\n            cy={px(knuckle.y)}\n            r={px((item.radii[0] ?? 4) * 0.78)}\n            {...machined}\n          />\n        )\n      })}\n      <circle\n        data-port\n        cx={px(to({ x: 0, y: 26, z: pose.palmFront }).x)}\n        cy={px(to({ x: 0, y: 26, z: pose.palmFront }).y)}\n        r={4.2}\n        fill={palette.accent}\n      />\n    </g>\n  )\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={`Robot ${side} hand, ${shown} grasp, ${readout} percent closed, ${viewNames[view] ?? viewNames.front}`}\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 100 : undefined}\n      aria-valuenow={interactive ? readout : undefined}\n      aria-valuetext={interactive ? `${readout}% closed, ${shown} grasp` : 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, 0.05, 0.2)\n        if (delta !== 0) apply(master + delta)\n        else if (event.key === \"Home\") apply(0)\n        else if (event.key === \"End\") apply(1)\n        else if (event.key === \"Enter\" || event.key === \" \") step()\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      data-view={view}\n      data-side={side}\n      {...props}\n    >\n      <g\n        data-hand\n        transform={`translate(${CENTRE.x} ${CENTRE.y})${fit === 1 ? \"\" : ` scale(${fit})`}`}\n      >\n        {showWrist && (\n          <g data-wrist>\n            <path d={extrudedPath(circleFootprint(0, 0, 13, 12), camera, 2, -16)} {...machined} />\n            <path d={extrudedPath(circleFootprint(0, 0, 15.5, 12), camera, -16, -22)} {...cast} />\n            <path d={extrudedPath(circleFootprint(0, 0, 13.5, 12), camera, -22, -38)} {...shell} />\n            <path\n              d={line({ x: -12, y: -30, z: 13 }, { x: 12, y: -30, z: 13 })}\n              stroke={palette.dark}\n              strokeWidth={1.1}\n              fill=\"none\"\n            />\n          </g>\n        )}\n\n        {behind.map(digitPart)}\n        {palmFace}\n        {infront.map(digitPart)}\n\n        {showPinch && closed > 0 && (\n          <g data-pinch opacity={px(0.25 + closed * 0.75)}>\n            <path\n              d={line(pose.pinch.thumb, pose.pinch.finger)}\n              stroke={palette.glow}\n              strokeWidth={1.2}\n              strokeDasharray=\"2 2\"\n              fill=\"none\"\n            />\n          </g>\n        )}\n\n        {variant === \"blueprint\" && (\n          <g fill=\"none\" stroke={palette.grid} strokeWidth={0.5} opacity={0.7}>\n            <path d={line({ x: -40, y: 52, z: 0 }, { x: 40, y: 52, z: 0 })} strokeDasharray=\"2 3\" />\n            <path d={line({ x: 0, y: -6, z: 0 }, { x: 0, y: 108, z: 0 })} strokeDasharray=\"2 3\" />\n          </g>\n        )}\n      </g>\n      <g fontFamily=\"ui-monospace, monospace\" textAnchor=\"middle\" fill={palette.foreground}>\n        <text x={VIEW_WIDTH / 2} y={172} fontSize={5}>\n          {`${shown.toUpperCase()} / ${readout}% / GAP ${px(gap).toFixed(1)}`}\n        </text>\n        {label && (\n          <text x={VIEW_WIDTH / 2} y={181} fontSize={4.5}>\n            {label}\n          </text>\n        )}\n      </g>\n    </svg>\n  )\n}\n\nexport { RobotHand, handGoal, handWave }\n",
      "type": "registry:ui",
      "target": "@ui/robot-hand.tsx"
    }
  ],
  "categories": [
    "robotics",
    "body"
  ],
  "type": "registry:ui"
}