{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "solenoid-valve",
  "title": "Solenoid valve",
  "description": "A coil-driven plunger switching a two- or three-port valve gallery, with a visible return spring, flow route, and four projected camera views.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/electromagnetism-geometry.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/solenoid-valve.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { coilWinding } from \"@/lib/robocn/electromagnetism\"\nimport { clamp } from \"@/lib/robocn/kinematics\"\nimport {\n  aboutPoint,\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 { cn } from \"@/lib/utils\"\n\nexport type SolenoidValveBehavior = \"cycle\" | \"pulse\" | \"static\"\n\nexport interface SolenoidValveProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  position?: number\n  onPositionChange?: (position: number) => void\n  ports?: 2 | 3\n  normally?: \"open\" | \"closed\"\n  behavior?: SolenoidValveBehavior\n  speed?: number\n  phase?: number\n  paused?: boolean\n  animate?: boolean\n  interactive?: boolean\n  showField?: boolean\n  label?: string\n  view?: RobotView\n  size?: RobotSize | number\n  variant?: RobotVariant\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\nconst winding = coilWinding({ turns: 9, length: 64, radius: 12, samplesPerTurn: 5 })\nconst windingPath = winding.map((point, index) => `${index ? \"L\" : \"M\"} ${px(82 + point.x)} ${px(79 + point.y)}`).join(\" \")\n\nfunction SolenoidValve({\n  position,\n  onPositionChange,\n  ports = 2,\n  normally = \"closed\",\n  behavior = \"cycle\",\n  speed = 0.35,\n  phase = 0,\n  paused = false,\n  animate = true,\n  interactive = false,\n  showField = true,\n  label,\n  view = \"profile\",\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  \"aria-label\": ariaLabel,\n  ...props\n}: SolenoidValveProps) {\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  const controlled = position !== undefined\n  const supplied = controlled && Number.isFinite(position) ? clamp(position, 0, 1) : controlled ? 0 : held\n  const goal = React.useCallback((clock: number) => solenoidValveGoal(behavior, clock), [behavior])\n  const motion = useRobotScalar(goal, {\n    hold: supplied,\n    rate: 2.5,\n    speed,\n    phase,\n    paused,\n    animate: animate && !controlled && behavior !== \"static\",\n  })\n  const value = clamp(motion.value, 0, 1)\n  const apply = React.useCallback((next: number) => {\n    const bounded = Math.round(clamp(next, 0, 1) * 100) / 100\n    setHeld(bounded)\n    onPositionChange?.(bounded)\n  }, [onPositionChange])\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    onDrag: React.useCallback((unit) => apply((unit.x - 0.28) / 0.48), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), []),\n  })\n  const open = normally === \"open\" ? value < 0.5 : value >= 0.5\n  const shift = px(value * 42)\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const camera = robotCamera(view)\n  const face = aboutPoint(camera.wall(0, 90), 120, 105, view === \"plan\" ? 0.92 : 1)\n  const springStart = 151 + value * 42\n  const spring = Array.from({ length: 9 }, (_, index) => {\n    const x = springStart + ((201 - springStart) * index) / 8\n    const y = index === 0 || index === 8 ? 79 : 79 + (index % 2 ? -7 : 7)\n    return `${index ? \"L\" : \"M\"} ${px(x)} ${y}`\n  }).join(\" \")\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={ariaLabel ?? `Solenoid valve, ${Math.round(value * 100)} percent, ${viewNames[view]}`}\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 1 : undefined}\n      aria-valuenow={interactive ? px(value) : undefined}\n      aria-valuetext={interactive ? `${Math.round(value * 100)} percent` : 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.15)\n        if (delta) apply(value + 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 240 160\"\n      width={width}\n      height={px(width * 2 / 3)}\n      className={cn(\"max-w-full select-none\", interactive && \"touch-none cursor-grab focus-visible:outline-2\", dragging && \"cursor-grabbing\", className)}\n      style={{ color: palette.foreground, ...style }}\n      {...props}\n    >\n      {variant === \"blueprint\" && <path d=\"M 12 128 H 228 M 120 18 V 138\" stroke={palette.grid} strokeWidth={0.6} strokeDasharray=\"3 4\" />}\n      {view !== \"profile\" && (\n        <g transform=\"translate(120 116)\" opacity={0.72}>\n          <path d={extrudedPath(roundedFootprint(102, 17, 4), camera, 72, 0)} {...shell} />\n        </g>\n      )}\n      <g data-view={view} transform={face || undefined}>\n        <rect x={17} y={45} width={206} height={70} rx={9} {...shell} />\n        <path d=\"M 28 58 H 210 M 28 101 H 210\" stroke={palette.dark} strokeWidth={2} opacity={0.45} />\n        <rect x={42} y={57} width={81} height={44} rx={5} {...cast} />\n        <path data-coil d={windingPath} fill=\"none\" stroke={palette.shell} strokeWidth={4} strokeLinecap=\"round\" />\n        <path d=\"M 40 108 V 124 H 126 V 108\" fill=\"none\" stroke={palette.metal} strokeWidth={3} />\n        <path d=\"M 49 124 v 12 M 118 124 v 12\" stroke={palette.accent} strokeWidth={2} />\n        <g data-plunger transform={`translate(${shift} 0)`}>\n          <rect x={106} y={70} width={45} height={18} rx={5} {...machined} />\n          <path d=\"M 148 65 V 93\" stroke={palette.dark} strokeWidth={5} strokeLinecap=\"round\" />\n        </g>\n        <path data-spring d={spring} fill=\"none\" stroke={palette.metal} strokeWidth={2.5} strokeLinejoin=\"round\" />\n        <rect x={201} y={61} width={11} height={36} rx={3} {...cast} />\n        {Array.from({ length: ports }, (_, index) => {\n          const x = ports === 2 ? 152 + index * 38 : 143 + index * 29\n          return <g key={x} data-port={index}><path d={`M ${x} 113 V 140`} stroke={palette.metal} strokeWidth={9} /><circle cx={x} cy={140} r={6} {...cast} /></g>\n        })}\n        <path\n          data-flow\n          data-open={open}\n          d={open ? \"M 151 132 V 106 Q 151 96 163 96 H 190 V 132\" : \"M 151 132 V 116 M 190 132 V 116\"}\n          fill=\"none\"\n          stroke={open ? palette.accent : palette.grid}\n          strokeWidth={3}\n          strokeDasharray={open ? \"5 3\" : undefined}\n        />\n        {showField && value > 0.03 && <g data-field fill=\"none\" stroke={palette.glow} opacity={0.25 + value * 0.55} strokeWidth={1.5}><ellipse cx={83} cy={79} rx={50} ry={29} /><ellipse cx={83} cy={79} rx={59} ry={36} /></g>}\n      </g>\n      {label && <text x={120} y={155} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\" fontSize={6} fill={palette.foreground}>{label}</text>}\n    </svg>\n  )\n}\n\nexport function solenoidValveGoal(behavior: SolenoidValveBehavior, clock: number) {\n  if (behavior === \"static\") return 0\n  const t = Number.isFinite(clock) ? ((clock % 1) + 1) % 1 : 0\n  if (behavior === \"pulse\") return t < 0.42 ? 0 : t < 0.52 ? (t - 0.42) * 10 : t < 0.84 ? 1 : 1 - (t - 0.84) / 0.16\n  return 0.5 - 0.5 * Math.cos(t * Math.PI * 2)\n}\n\nexport { SolenoidValve }\n",
      "type": "registry:ui",
      "target": "@ui/solenoid-valve.tsx"
    }
  ],
  "categories": [
    "robotics",
    "actuators"
  ],
  "type": "registry:ui"
}