{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slate-tablet",
  "title": "Slate tablet",
  "description": "A slate and the kickstand holding it up: one recline axis leans the slate and solves the stand's foot onto the desk, and a leg too short to reach folds flat instead of stretching. Docked stylus, four camera angles.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/device-geometry.json",
    "https://robocn.dev/r/use-robot-motion.json"
  ],
  "files": [
    {
      "path": "src/components/ui/slate-tablet.tsx",
      "content": "\"use client\"\n\n/**\n * slate-tablet — a slate and the stand holding it up.\n *\n * `recline` is one axis moving two things: how far the slate leans back, and\n * the stand that has to close the triangle under it. The foot is solved, not\n * drawn — given the drop from the hinge to the desk, the horizontal run is\n * whatever is left of the leg.\n *\n * Shorten the leg past what the tilt needs and the stand folds flat instead of\n * stretching, and the label says so. A drawing that let the leg grow would be\n * lying about the only mechanism it has.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { panelTransform, standPose } from \"@/lib/robocn/device\"\nimport { clamp, convexHull2, toRadians, type Vec2 } from \"@/lib/robocn/kinematics\"\nimport {\n  capsulePath,\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\nconst VIEW_WIDTH = 190\nconst VIEW_HEIGHT = 172\n/** Where the slate's bottom edge, on the desk, lands in the frame. */\nconst ORIGIN = { x: 95, y: 118 }\n\n/** World units: x starboard, y up, z toward the back. */\nconst HALF_W = 52\nconst SLATE = 78\nconst SLATE_T = 4.4\n/** How far up the back of the slate the stand is hinged. */\nconst MOUNT = 44\nconst STAND_HALF_W = 26\n/** Recline (0–1) per second while easing back into the behaviour. */\nconst SLEW_RATE = 0.8\nconst NATIVE_VIEW: RobotView = \"profile\"\n\nconst frames: Record<RobotView, { zoom: number; dx: number; dy: number }> = {\n  plan: { zoom: 0.88, dx: 0, dy: -44 },\n  front: { zoom: 0.94, dx: 0, dy: 28 },\n  profile: { zoom: 0.94, dx: 34, dy: 24 },\n  iso: { zoom: 0.74, dx: 24, dy: 34 },\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 type TabletBehavior = \"prop\" | \"sketch\" | \"static\"\nexport type TabletScreen = \"home\" | \"sketch\" | \"off\"\n\nexport interface SlateTabletProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** Controlled recline, 0 nearly upright to 1 laid right back. */\n  recline?: number\n  /** What the slate does when `recline` is not supplied. */\n  behavior?: TabletBehavior\n  /** Stand leg length in world units, clamped 20–80. Short legs fold. */\n  leg?: number\n  /** Where the camera stands. One slate, four projections. */\n  view?: RobotView\n  /** Recline cycles per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** Drag the slate back, or arrow-key it. */\n  interactive?: boolean\n  onReclineChange?: (recline: number) => void\n  /** What the display is showing. Structure only — no application artwork. */\n  screen?: TabletScreen\n  /** Dock the stylus along the top edge. */\n  stylus?: boolean\n  showDesk?: boolean\n  label?: string\n  size?: RobotSize | number\n  variant?: RobotVariant\n}\n\nfunction SlateTablet({\n  recline,\n  behavior = \"prop\",\n  leg = 46,\n  view = NATIVE_VIEW,\n  speed = 0.22,\n  animate = true,\n  paused = false,\n  phase = 0,\n  interactive = false,\n  onReclineChange,\n  screen = \"home\",\n  stylus = true,\n  showDesk = 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}: SlateTabletProps) {\n  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })\n  const width = resolveRobotSize(size)\n  const legLength = Number.isFinite(leg) ? clamp(leg, 20, 80) : 46\n  const controlled = recline !== undefined\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const [held, setHeld] = React.useState<number | null>(null)\n\n  const hold = controlled ? (Number.isFinite(recline) ? clamp(recline, 0, 1) : 0) : held\n  const goal = React.useCallback((clock: number) => tabletGoal(behavior, clock), [behavior])\n  const motion = useRobotScalar(goal, {\n    rate: SLEW_RATE,\n    hold,\n    speed,\n    animate: animate && !controlled && behavior !== \"static\",\n    paused,\n    phase,\n  })\n  const at01 = 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      onReclineChange?.(bounded)\n    },\n    [onReclineChange, setHeld],\n  )\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    // Down the frame lays it back, the way a hand pushes the top edge over.\n    onDrag: React.useCallback((unit: Vec2) => apply((unit.y * VIEW_HEIGHT - 30) / 86), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), [setHeld]),\n  })\n\n  const pose = standPose(at01, SLATE, legLength, MOUNT)\n  const readout = Math.round(at01 * 100)\n\n  const camera = robotCamera(view)\n  const frame = frames[view] ?? frames.profile\n\n  // The solver works in the slate's own side elevation — x toward the back,\n  // y down. Lift it into world units: that plane is the machine's z–y plane.\n  const world = (planar: Vec2, x: number) => ({ x, y: -planar.y, z: planar.x })\n  const tilt = toRadians(pose.tilt)\n  // Out of the front face of the slate, in the same plane.\n  const normal = { x: -Math.cos(tilt), y: -Math.sin(tilt) }\n  const faceOffset = (planar: Vec2, side: number) => ({\n    x: planar.x + normal.x * side * (SLATE_T / 2),\n    y: planar.y + normal.y * side * (SLATE_T / 2),\n  })\n  const foot0 = { x: 0, y: 0 }\n\n  const corners = [1, -1].flatMap((side) =>\n    [foot0, pose.top].flatMap((planar) => {\n      const shifted = faceOffset(planar, side)\n      return [HALF_W, -HALF_W].map((x) => world(shifted, x))\n    }),\n  )\n  const hull = convexHull2(corners.map((c) => camera.project(c.x, c.y, c.z)))\n  const body = hull.length\n    ? `${hull.map((p, i) => `${i ? \"L\" : \"M\"} ${px(p.x)} ${px(p.y)}`).join(\" \")} Z`\n    : \"\"\n\n  const frontTop = world(faceOffset(pose.top, 1), HALF_W)\n  const frontTopPort = world(faceOffset(pose.top, 1), -HALF_W)\n  const frontFoot = world(faceOffset(foot0, 1), HALF_W)\n  const display = panelTransform(camera, frontTop, frontTopPort, frontFoot, HALF_W * 2, SLATE)\n  const showScreen = display.facing > 0.25\n\n  // The stand is a plate between the hinge line and the foot line, mounted on\n  // the back surface rather than on the centreline — which is what makes a\n  // folded stand visible lying against the slate instead of buried inside it.\n  const onBack = (planar: Vec2) => ({\n    x: planar.x - normal.x * (SLATE_T / 2 + 0.7),\n    y: planar.y - normal.y * (SLATE_T / 2 + 0.7),\n  })\n  const standHinge = onBack(pose.hinge)\n  const standFoot = pose.folded ? onBack(pose.foot) : pose.foot\n  const standCorners = [\n    world(standHinge, STAND_HALF_W),\n    world(standHinge, -STAND_HALF_W),\n    world(standFoot, -STAND_HALF_W),\n    world(standFoot, STAND_HALF_W),\n  ].map((c) => camera.project(c.x, c.y, c.z))\n  const standHull = convexHull2(standCorners)\n  const stand = standHull.length\n    ? `${standHull.map((p, i) => `${i ? \"L\" : \"M\"} ${px(p.x)} ${px(p.y)}`).join(\" \")} Z`\n    : \"\"\n\n  // The stylus docks along the top edge, on the back face.\n  const dock = world(faceOffset(pose.top, -1), 0)\n  const penA = camera.project(-30, dock.y, dock.z)\n  const penB = camera.project(30, dock.y, dock.z)\n  const shadow = extrudedPath(\n    roundedFootprint(HALF_W + 6, Math.max(12, (pose.foot.x + 10) / 2), 10, 5).map((p) => ({\n      x: p.x,\n      y: p.y + Math.max(12, (pose.foot.x + 10) / 2) - 6,\n    })),\n    camera,\n    0,\n    0,\n  )\n\n  // Whichever of the stand and the slate is nearer the camera paints last.\n  const standNearer =\n    camera.depth(0, -((standHinge.y + standFoot.y) / 2), (standHinge.x + standFoot.x) / 2) >\n    camera.depth(0, -pose.top.y / 2, pose.top.x / 2)\n\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const glass = variant === \"solid\" ? { fill: palette.dark } : robotSurface(\"dark\", variant, palette, 0.8)\n\n  const standGroup = (\n    <path data-stand data-folded={pose.folded ? \"true\" : \"false\"} d={stand} {...(pose.folded ? cast : machined)} />\n  )\n  const slateGroup = (\n    <g data-slate>\n      <path data-body d={body} {...shell} />\n      {showScreen && (\n        <g data-screen data-content={screen} transform={display.transform}>\n          <rect x={4} y={4} width={96} height={70} rx={3} {...glass} />\n          <circle cx={52} cy={2.4} r={0.9} fill={palette.metal} />\n          {screen !== \"off\" && <ScreenContent screen={screen} palette={palette} />}\n        </g>\n      )}\n      {stylus && <path data-stylus d={capsulePath(penA, penB, 2.1)} {...machined} />}\n    </g>\n  )\n\n  return (\n    <svg\n      ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={`Slate tablet, ${readout} percent reclined, ${pose.folded ? \"stand folded\" : \"stand propped\"}, ${screen} screen, ${viewNames[view] ?? viewNames.profile}`}\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 100 : undefined}\n      aria-valuenow={interactive ? readout : undefined}\n      aria-valuetext={interactive ? `${readout}% reclined` : 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(at01 + 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      data-view={view}\n      {...props}\n    >\n      <g transform={`translate(${px(ORIGIN.x + frame.dx)} ${px(ORIGIN.y + frame.dy)}) scale(${frame.zoom})`}>\n        {showDesk && <path data-desk d={shadow} fill={palette.dark} opacity={0.13} />}\n        {variant === \"blueprint\" && (\n          <g fill=\"none\" stroke={palette.grid} strokeWidth={0.6} opacity={0.6}>\n            <path\n              d={`M ${px(camera.project(-HALF_W - 14, 0, 0).x)} ${px(camera.project(-HALF_W - 14, 0, 0).y)} L ${px(camera.project(HALF_W + 14, 0, pose.foot.x + 12).x)} ${px(camera.project(HALF_W + 14, 0, pose.foot.x + 12).y)}`}\n              strokeDasharray=\"5 3\"\n            />\n          </g>\n        )}\n        {standNearer ? (\n          <>\n            {slateGroup}\n            {standGroup}\n          </>\n        ) : (\n          <>\n            {standGroup}\n            {slateGroup}\n          </>\n        )}\n      </g>\n      <g fontFamily=\"ui-monospace, monospace\" textAnchor=\"middle\" fill={palette.foreground}>\n        <text x={VIEW_WIDTH / 2} y={VIEW_HEIGHT - 9} fontSize={5}>\n          {`${Math.round(pose.tilt)}° / ${pose.folded ? \"FOLDED\" : `${Math.round(pose.spread)}° SPREAD`}`}\n        </text>\n        {label && (\n          <text x={VIEW_WIDTH / 2} y={VIEW_HEIGHT - 2.5} fontSize={4.5}>\n            {label}\n          </text>\n        )}\n      </g>\n    </svg>\n  )\n}\n\n/** Structure drawn in palette roles, never an application's own artwork. */\nfunction ScreenContent({\n  screen,\n  palette,\n}: {\n  screen: TabletScreen\n  palette: ReturnType<typeof resolveRobotPalette>\n}) {\n  if (screen === \"sketch\") {\n    return (\n      <g>\n        <rect x={8} y={8} width={10} height={62} rx={3} fill={palette.metal} opacity={0.3} />\n        {[0, 1, 2, 3].map((slot) => (\n          <circle key={slot} cx={13} cy={15 + slot * 11} r={2.6} fill={slot === 1 ? palette.accent : palette.metal} opacity={slot === 1 ? 1 : 0.6} />\n        ))}\n        <path\n          d=\"M 26 56 C 38 20, 54 66, 66 32 S 86 22, 92 44\"\n          fill=\"none\"\n          stroke={palette.accent}\n          strokeWidth={2.2}\n          strokeLinecap=\"round\"\n        />\n        <path d=\"M 26 66 H 92\" stroke={palette.metal} strokeWidth={1} opacity={0.4} />\n      </g>\n    )\n  }\n  return (\n    <g>\n      {Array.from({ length: 16 }, (_, index) => (\n        <rect\n          key={index}\n          x={16 + (index % 4) * 19}\n          y={12 + Math.floor(index / 4) * 14}\n          width={13}\n          height={10}\n          rx={2.6}\n          fill={index === 5 ? palette.accent : palette.metal}\n          opacity={index === 5 ? 1 : 0.42}\n        />\n      ))}\n      <rect x={22} y={62} width={60} height={10} rx={4} fill={palette.metal} opacity={0.25} />\n      {[0, 1, 2, 3].map((slot) => (\n        <rect key={slot} x={27 + slot * 13} y={64} width={7} height={6} rx={1.8} fill={palette.metal} opacity={0.7} />\n      ))}\n    </g>\n  )\n}\n\n/**\n * Recline at `clock`, 0–1. `prop` stands it up, holds it there and lays it\n * back down; `sketch` works the small range someone drawing on it uses.\n */\nexport function tabletGoal(behavior: TabletBehavior, clock: number) {\n  if (behavior === \"static\" || !Number.isFinite(clock)) return 0\n  const t = ((clock % 1) + 1) % 1\n  if (behavior === \"sketch\") return 0.68 + Math.sin(t * Math.PI * 2) * 0.1\n  if (t < 0.2) return (t / 0.2) * 0.88\n  if (t < 0.75) return 0.88\n  if (t < 0.92) return 0.88 * (1 - (t - 0.75) / 0.17)\n  return 0\n}\n\nexport { SlateTablet }\n",
      "type": "registry:ui",
      "target": "@ui/slate-tablet.tsx"
    }
  ],
  "categories": [
    "robotics",
    "devices"
  ],
  "type": "registry:ui"
}