{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "refrigerator",
  "title": "Refrigerator",
  "description": "A cabinet whose doors are solved leaves on vertical hinges, with an interior that is a second drawing revealed by the swing and a lamp thrown by a real door switch. Top-freezer, side-by-side or single.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/household-geometry.json",
    "https://robocn.dev/r/use-robot-motion.json",
    "https://robocn.dev/r/device-geometry.json"
  ],
  "files": [
    {
      "path": "src/components/ui/refrigerator.tsx",
      "content": "\"use client\"\n\n/**\n * refrigerator — a cabinet whose doors are solved, and an inside worth opening.\n *\n * A leaf hinged on a vertical edge is a flat panel at an attitude, so the door\n * goes through `swingPose` for its free edge and `panelTransform` for its own\n * artwork: the handle and the display ride the leaf, foreshorten as it opens,\n * and vanish when it is edge-on. Nothing is drawn twice for a second camera.\n *\n * The lamp is a real door switch: it makes at a stated angle, so the inside\n * lights the moment the seal breaks rather than when an animation says so. The\n * interior — shelves, drawers, what is on them — is a second drawing, revealed\n * by the swing.\n *\n * There is no thermodynamics here. The compressor duty cycle is a pattern on\n * the clock, the temperatures on the display are labels, and nothing computes a\n * heat load, a defrost or a door-open penalty.\n */\n\nimport * as React from \"react\"\n\nimport { clamp, type Vec2, type Vec3 } from \"@/lib/robocn/kinematics\"\nimport { panelPath, panelTransform } from \"@/lib/robocn/device\"\nimport { swingPose } from \"@/lib/robocn/household\"\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport {\n  boxCorners,\n  elevationDraft,\n  elevationPoint,\n  fitTransform,\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 RefrigeratorBehavior = \"service\" | \"idle\" | \"static\"\nexport type RefrigeratorLayout = \"top-freezer\" | \"side-by-side\" | \"single\"\n\nconst VIEW_WIDTH = 200\nconst VIEW_HEIGHT = 250\nconst NATIVE_VIEW: RobotView = \"front\"\n\nconst HALF = 40\nconst DEPTH = 26\nconst FOOT = 4\nconst TOP = 162\n/** Where the two compartments meet in a top-freezer cabinet. */\nconst SPLIT = 104\nconst GASKET = 4\nconst FACE = 0.5\n/** How far a door can swing before it hits its stop. */\nconst MAX_ANGLE = 110\n/** The door switch makes here: past this the lamp is on. */\nconst SWITCH = 0.05\n/** A door is a slab, not a sheet: this is what gives it an edge off-axis. */\nconst LEAF_THICKNESS = 4\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 RefrigeratorProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Controlled fresh-food door, 0 shut to 1 at its stop. Stops the loop. */\n  door?: number\n  onDoorChange?: (door: number) => void\n  /** Controlled freezer door. Independent of the fresh one. */\n  freezer?: number\n  behavior?: RefrigeratorBehavior\n  layout?: RefrigeratorLayout\n  /** Shelves in the fresh compartment, 2–5. */\n  shelves?: number\n  /** Draw the condenser coil and the compressor at the back. */\n  showPlant?: boolean\n  showGround?: boolean\n  view?: RobotView\n  speed?: number\n  phase?: number\n  paused?: boolean\n  animate?: boolean\n  interactive?: boolean\n  label?: string\n  size?: RobotSize | number\n  variant?: RobotVariant\n}\n\nfunction Refrigerator({\n  door,\n  onDoorChange,\n  freezer,\n  behavior = \"service\",\n  layout = \"top-freezer\",\n  shelves = 3,\n  showPlant = true,\n  showGround = true,\n  view = NATIVE_VIEW,\n  speed = 0.18,\n  phase = 0,\n  paused = false,\n  animate = true,\n  interactive = false,\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  \"aria-label\": ariaLabel,\n  ...props\n}: RefrigeratorProps) {\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 = door !== undefined\n\n  const racks = Number.isFinite(shelves) ? clamp(Math.round(shelves), 2, 5) : 3\n  const hold = controlled ? (Number.isFinite(door) ? clamp(door as number, 0, 1) : 0) : held\n  const goal = React.useCallback(\n    (clock: number) => fridgeDoor(behavior, clock),\n    [behavior],\n  )\n  const motion = useRobotScalar(goal, {\n    rate: 1.6,\n    hold,\n    speed,\n    paused,\n    phase,\n    animate: animate && !controlled && behavior !== \"static\",\n  })\n  const fresh = clamp(motion.value, 0, 1)\n  const cold = clamp(\n    freezer !== undefined && Number.isFinite(freezer)\n      ? freezer\n      : fridgeFreezer(behavior, motion.clock),\n    0,\n    1,\n  )\n  const running = fridgeDuty(motion.clock)\n\n  const apply = React.useCallback(\n    (next: number) => {\n      const bounded = Math.round(clamp(next, 0, 1) * 1000) / 1000\n      setHeld(bounded)\n      onDoorChange?.(bounded)\n    },\n    [onDoorChange],\n  )\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    onDrag: React.useCallback((unit: Vec2) => apply(1 - unit.x), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), []),\n  })\n  // Report the door as the behaviour swings it, not only under a pointer.\n  const reported = React.useRef(-1)\n  React.useEffect(() => {\n    const at = Math.round(fresh * 100) / 100\n    if (reported.current === at) return\n    reported.current = at\n    onDoorChange?.(at)\n  }, [fresh, onDoorChange])\n\n  const camera = robotCamera(view)\n  const envelope = boxCorners(\n    { x: -(HALF + 10), y: 0, z: -(DEPTH + 6) },\n    { x: HALF + 10, y: TOP + 8, z: DEPTH + HALF * 1.4 },\n  )\n  const frame = fitTransform(envelope, camera, VIEW_WIDTH, VIEW_HEIGHT)\n  const { point: to, box, bar, disc } = elevationDraft(camera, \"front\")\n\n  const wash = (value: number) => (variant === \"outline\" || variant === \"wire\" ? 0 : value)\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n\n  const leaves = doorLeaves(layout, fresh, cold)\n  const lamp = leaves.some((leaf) => leaf.compartment === \"fresh\" && leaf.open > SWITCH)\n  const frozenLamp = leaves.some((leaf) => leaf.compartment === \"freezer\" && leaf.open > SWITCH)\n\n  const percent = Math.round(fresh * 100)\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={\n        ariaLabel ??\n        `Refrigerator, ${layout.replace(\"-\", \" \")}, door ${percent} percent open, ${viewNames[view] ?? viewNames.front}`\n      }\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 1 : undefined}\n      aria-valuenow={interactive ? px(fresh) : undefined}\n      aria-valuetext={interactive ? `door ${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, 0.05, 0.2)\n        if (delta !== 0) apply(fresh + 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        <path\n          d={`M 8 ${VIEW_HEIGHT - 14} H ${VIEW_WIDTH - 8}`}\n          fill=\"none\"\n          stroke={palette.grid}\n          strokeWidth={0.5}\n          strokeDasharray=\"3 4\"\n          opacity={0.45}\n        />\n      )}\n\n      <g data-cabinet data-view={view} transform={frame || undefined}>\n        {showGround && (\n          <path\n            data-ground\n            d={box(-HALF - 4, 0, HALF + 4, 0.4, DEPTH + 4)}\n            fill={palette.dark}\n            opacity={0.14}\n          />\n        )}\n\n        {/* Plant at the back: the coil and the compressor that runs it. */}\n        {showPlant && (\n          <g data-plant>\n            <path\n              data-coil\n              d={coilPath(to, -DEPTH - 3)}\n              fill=\"none\"\n              stroke={palette.metal}\n              strokeWidth={1}\n              opacity={0.75}\n            />\n            <path\n              data-compressor\n              d={disc({ x: -18, y: 14 }, 9, 7, -DEPTH + 6)}\n              fill={running ? palette.accent : palette.metal}\n              fillOpacity={wash(running ? 0.6 : 1)}\n              stroke={palette.dark}\n              strokeWidth={0.6}\n            />\n          </g>\n        )}\n\n        {/* Carcass: the box, then the lit cavity inside it. */}\n        <path data-carcass d={box(-HALF, FOOT, HALF, TOP, DEPTH)} {...shell} />\n        <path\n          data-cavity\n          d={box(-HALF + 3, FOOT + 3, HALF - 3, TOP - 3, FACE, DEPTH - 3)}\n          fill={lamp || frozenLamp ? palette.metal : palette.dark}\n          fillOpacity={wash(lamp || frozenLamp ? 0.6 : 0.72)}\n          stroke={palette.dark}\n          strokeWidth={0.5}\n        />\n\n        {/* Shelves, drawer and what is on them: only worth drawing once a door\n            is open far enough to see past the leaf. */}\n        {(lamp || frozenLamp) && (\n          <g data-interior>\n            {layout === \"top-freezer\" && (\n              <path\n                d={box(-HALF + 3, SPLIT, HALF - 3, SPLIT + GASKET, FACE, DEPTH - 4)}\n                {...cast}\n              />\n            )}\n            {Array.from({ length: racks }, (_, index) => {\n              // The fresh compartment is the whole cabinet unless a freezer is\n              // sitting on top of it, so the shelves have to know the layout.\n              const ceiling = layout === \"top-freezer\" ? SPLIT : TOP - 6\n              const y = FOOT + 26 + (index * (ceiling - FOOT - 40)) / racks\n              return (\n                <path\n                  key={index}\n                  data-shelf={index}\n                  d={box(-HALF + 6, y, HALF - 6, y + 1.4, FACE, DEPTH - 6)}\n                  {...machined}\n                />\n              )\n            })}\n            <path\n              data-drawer\n              d={box(-HALF + 6, FOOT + 6, HALF - 6, FOOT + 22, FACE, DEPTH - 5)}\n              fill={palette.metal}\n              fillOpacity={wash(0.45)}\n              stroke={palette.dark}\n              strokeWidth={0.5}\n            />\n            {[-22, -6, 12].map((x, index) => (\n              <path\n                key={x}\n                d={box(x, FOOT + 28 + index * 0.5, x + 9, FOOT + 44, FACE, DEPTH - 8)}\n                fill={palette.accent}\n                fillOpacity={wash(0.7)}\n                stroke={palette.dark}\n                strokeWidth={0.4}\n              />\n            ))}\n            <circle\n              data-lamp\n              cx={px(to({ x: 0, y: SPLIT - 8 }, DEPTH - 5).x)}\n              cy={px(to({ x: 0, y: SPLIT - 8 }, DEPTH - 5).y)}\n              r={2.6}\n              fill={palette.accent}\n            />\n          </g>\n        )}\n\n        {/* The doors themselves, each a solved leaf with its own artwork. */}\n        {leaves.map((leaf) => {\n          const pose = swingPose(leaf.open, {\n            width: leaf.width,\n            maxAngle: MAX_ANGLE,\n            side: leaf.side,\n          })\n          const hinge = { x: leaf.hinge, y: leaf.top }\n          // The leaf's own x runs the way a reader of it sees it — left to\n          // right on screen when the door is shut — so `facing` is positive for\n          // its front face and turns negative only when it has swung past the\n          // camera and shown its inside.\n          const edge = { x: leaf.hinge + pose.edge.x, depth: DEPTH + pose.edge.depth }\n          const near = leaf.side === -1 ? edge : { x: leaf.hinge, depth: DEPTH }\n          const far = leaf.side === -1 ? { x: leaf.hinge, depth: DEPTH } : edge\n          const corner = elevationPoint({ x: near.x, y: leaf.top }, near.depth, \"front\")\n          const along = elevationPoint({ x: far.x, y: leaf.top }, far.depth, \"front\")\n          const down = elevationPoint({ x: near.x, y: leaf.bottom }, near.depth, \"front\")\n          const height = leaf.top - leaf.bottom\n          const panel = panelTransform(camera, corner, along, down, leaf.width, height)\n          const slab = leafSlab(camera, hinge, leaf, pose, height)\n          return (\n            <g key={leaf.key} data-door={leaf.key} data-open={px(leaf.open)}>\n              <path d={slab} {...shell} />\n              <path d={panelPath(camera, corner as Vec3, along as Vec3, down as Vec3)} {...shell} />\n              {panel.facing > 0.12 && (\n                <g transform={panel.transform || undefined}>\n                  <rect\n                    x={leaf.width * 0.08}\n                    y={height * 0.06}\n                    width={leaf.width * 0.84}\n                    height={height * 0.88}\n                    fill=\"none\"\n                    stroke={palette.dark}\n                    strokeWidth={0.5}\n                    opacity={0.35}\n                  />\n                  <rect\n                    data-handle\n                    x={leaf.side === 1 ? leaf.width * 0.78 : leaf.width * 0.12}\n                    y={height * 0.12}\n                    width={leaf.width * 0.1}\n                    height={height * 0.42}\n                    rx={leaf.width * 0.05}\n                    fill={palette.metal}\n                    stroke={palette.dark}\n                    strokeWidth={0.4}\n                  />\n                  {leaf.compartment === \"fresh\" && (\n                    <rect\n                      data-display\n                      x={leaf.width * 0.2}\n                      y={height * 0.06}\n                      width={leaf.width * 0.3}\n                      height={height * 0.07}\n                      fill={palette.accent}\n                      fillOpacity={wash(running ? 0.9 : 0.5)}\n                    />\n                  )}\n                </g>\n              )}\n            </g>\n          )\n        })}\n\n        {/* The kick plate the cabinet stands on. */}\n        <path data-plinth d={box(-HALF + 3, 0, HALF - 3, FOOT, DEPTH - 3)} {...cast} />\n\n        {variant === \"blueprint\" && (\n          <>\n            <path\n              d={bar(\n                { x: HALF + 6, y: FOOT },\n                { x: HALF + 6, y: TOP },\n                0.2,\n                0.2,\n              )}\n              fill=\"none\"\n              stroke={palette.grid}\n              strokeWidth={0.5}\n              strokeDasharray=\"2 2\"\n            />\n            <text\n              x={px(to({ x: 0, y: TOP + 6 }).x)}\n              y={px(to({ x: 0, y: TOP + 6 }).y)}\n              textAnchor=\"middle\"\n              fontFamily=\"ui-monospace, monospace\"\n              fontSize={6}\n              fill={palette.foreground}\n            >\n              {`${Math.round(fresh * MAX_ANGLE)}°`}\n            </text>\n          </>\n        )}\n      </g>\n\n      {label && (\n        <text\n          x={VIEW_WIDTH / 2}\n          y={VIEW_HEIGHT - 5}\n          textAnchor=\"middle\"\n          fontFamily=\"ui-monospace, monospace\"\n          fontSize={6}\n          fill={palette.foreground}\n        >\n          {label}\n        </text>\n      )}\n    </svg>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* geometry                                                                    */\n/* -------------------------------------------------------------------------- */\n\ninterface Leaf {\n  key: string\n  compartment: \"fresh\" | \"freezer\"\n  /** Hinge edge, in the drawing's own x. */\n  hinge: number\n  /** Which way the free edge sweeps. */\n  side: 1 | -1\n  width: number\n  top: number\n  bottom: number\n  open: number\n}\n\n/** Which leaves a layout has, and how far each of them is open. */\nfunction doorLeaves(\n  layout: RefrigeratorLayout,\n  fresh: number,\n  freezer: number,\n): Leaf[] {\n  if (layout === \"side-by-side\") {\n    return [\n      { key: \"freezer\", compartment: \"freezer\", hinge: -HALF, side: 1, width: HALF, top: TOP - 2, bottom: FOOT + 2, open: freezer },\n      { key: \"fresh\", compartment: \"fresh\", hinge: HALF, side: -1, width: HALF, top: TOP - 2, bottom: FOOT + 2, open: fresh },\n    ]\n  }\n  if (layout === \"single\") {\n    return [\n      { key: \"fresh\", compartment: \"fresh\", hinge: HALF, side: -1, width: HALF * 2, top: TOP - 2, bottom: FOOT + 2, open: fresh },\n    ]\n  }\n  return [\n    { key: \"freezer\", compartment: \"freezer\", hinge: HALF, side: -1, width: HALF * 2, top: TOP - 2, bottom: SPLIT + GASKET, open: freezer },\n    { key: \"fresh\", compartment: \"fresh\", hinge: HALF, side: -1, width: HALF * 2, top: SPLIT, bottom: FOOT + 2, open: fresh },\n  ]\n}\n\n/**\n * The door as the slab it is: the leaf's face, and the same face `LEAF_THICKNESS`\n * behind it along its own normal. That is what puts an edge on a door seen from\n * anywhere but square on.\n */\nfunction leafSlab(\n  camera: ReturnType<typeof robotCamera>,\n  hinge: Vec2,\n  leaf: Leaf,\n  pose: ReturnType<typeof swingPose>,\n  height: number,\n): string {\n  const width = Math.max(1e-6, leaf.width)\n  // Along the leaf, then its normal in the (x, depth) plane.\n  const ux = pose.edge.x / width\n  const ud = pose.edge.depth / width\n  const nx = -ud * LEAF_THICKNESS\n  const nd = ux * LEAF_THICKNESS\n  const corners: Vec3[] = []\n  for (const along of [0, width]) {\n    for (const back of [0, 1]) {\n      for (const y of [hinge.y, hinge.y - height]) {\n        corners.push(\n          elevationPoint(\n            { x: hinge.x + ux * along + nx * back, y },\n            DEPTH + ud * along + nd * back,\n            \"front\",\n          ),\n        )\n      }\n    }\n  }\n  return slabPath(corners, camera)\n}\n\n/** The condenser serpentine on the back panel: drawing, and it says so. */\nfunction coilPath(\n  to: ReturnType<typeof elevationDraft>[\"point\"],\n  depth: number,\n): string {\n  const rows = 7\n  return Array.from({ length: rows }, (_, index) => {\n    const y = 30 + index * 16\n    const left = index % 2 === 0\n    const a = to({ x: left ? -HALF + 6 : HALF - 6, y }, depth)\n    const b = to({ x: left ? HALF - 6 : -HALF + 6, y }, depth)\n    return `${index ? \"L\" : \"M\"} ${px(a.x)} ${px(a.y)} L ${px(b.x)} ${px(b.y)}`\n  }).join(\" \")\n}\n\n/* -------------------------------------------------------------------------- */\n/* behaviours                                                                  */\n/* -------------------------------------------------------------------------- */\n\n/** The fresh-food door through a service cycle: open, stand open, shut. */\nexport function fridgeDoor(behavior: RefrigeratorBehavior, clock: number): number {\n  if (behavior === \"static\" || !Number.isFinite(clock)) return 0.55\n  if (behavior === \"idle\") return 0\n  const t = ((clock % 1) + 1) % 1\n  if (t < 0.12) return t / 0.12\n  if (t < 0.34) return 1\n  if (t < 0.46) return 1 - (t - 0.34) / 0.12\n  return 0\n}\n\n/** The freezer door, opened after the fresh one and never at the same time. */\nexport function fridgeFreezer(behavior: RefrigeratorBehavior, clock: number): number {\n  if (behavior !== \"service\" || !Number.isFinite(clock)) return 0\n  const t = ((clock % 1) + 1) % 1\n  if (t < 0.58) return 0\n  if (t < 0.66) return (t - 0.58) / 0.08\n  if (t < 0.82) return 1\n  if (t < 0.9) return 1 - (t - 0.82) / 0.08\n  return 0\n}\n\n/**\n * Whether the compressor is running. A duty cycle on the clock, not a\n * thermostat: nothing here knows what temperature anything is.\n */\nexport function fridgeDuty(clock: number): boolean {\n  if (!Number.isFinite(clock)) return false\n  return (((clock * 1.7) % 1) + 1) % 1 < 0.45\n}\n\nexport { Refrigerator }\n",
      "type": "registry:ui",
      "target": "@ui/refrigerator.tsx"
    }
  ],
  "categories": [
    "robotics",
    "home"
  ],
  "type": "registry:ui"
}