{
  "name": "book",
  "title": "Book",
  "type": "registry:component",
  "description": "A single procedural book — standing, lying, or open — with a seeded cover and optional click interaction; the standalone sibling of Bookshelf's instanced spines.",
  "dependencies": [
    "@react-three/drei@^10.7.7",
    "@runek/core@^0.12.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Book.tsx",
      "content": "import { Html } from '@react-three/drei'\nimport { rng, useWorld, type Vec3 } from '@runek/core'\nimport { useMemo, useState } from 'react'\n\nexport type BookPose = 'standing' | 'lying' | 'open'\n\nexport interface BookProps {\n  position?: Vec3\n  rotation?: Vec3\n  /** Cover width (spine to fore-edge), in units. */\n  width?: number\n  /** Spine length, in units. */\n  height?: number\n  /** Closed page-block thickness, in units. */\n  thickness?: number\n  /**\n   * `lying` rests on its back cover (spine on the -x edge); `standing` stands\n   * upright on its bottom edge; `open` lies opened flat at the spine.\n   */\n  pose?: BookPose\n  /** Cover color. Defaults to a seeded cloth-bound color. */\n  color?: string\n  pageColor?: string\n  /** Shown as a hover label when the book is interactive. */\n  title?: string\n  /** Navigated to on click when no `onSelect` is given. */\n  href?: string\n  /** Called on click. Optional, so the book still renders from data. */\n  onSelect?: () => void\n  seed?: number\n}\n\n/**\n * A single procedural book — the standalone, placeable sibling of `Bookshelf`'s\n * instanced spines. Put it on a table, a pedestal, or a reading stand; give it\n * an `href` or `onSelect` and it becomes a clickable artifact. Decorative: no\n * collider.\n */\nexport function Book({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  width = 0.22,\n  height = 0.3,\n  thickness = 0.05,\n  pose = 'lying',\n  color,\n  pageColor = '#efe7d2',\n  title,\n  href,\n  onSelect,\n  seed = 1,\n}: BookProps) {\n  const { unit } = useWorld()\n  const [hovered, setHovered] = useState(false)\n\n  const w = width * unit\n  const h = height * unit\n  const t = thickness * unit\n  const board = Math.max(t * 0.16, 0.004 * unit)\n\n  const coverColor = useMemo(() => {\n    if (color) return color\n    const next = rng(seed)\n    return `hsl(${Math.round(next() * 360)}, ${Math.round(35 + next() * 20)}%, ${Math.round(\n      35 + next() * 15,\n    )}%)`\n  }, [color, seed])\n\n  const interactive = onSelect != null || href != null\n  const handlers = interactive\n    ? {\n        onClick: (e: { stopPropagation: () => void }) => {\n          e.stopPropagation()\n          if (onSelect) onSelect()\n          else if (href) window.location.href = href\n        },\n        onPointerOver: (e: { stopPropagation: () => void }) => {\n          e.stopPropagation()\n          setHovered(true)\n          document.body.style.cursor = 'pointer'\n        },\n        onPointerOut: () => {\n          setHovered(false)\n          document.body.style.cursor = 'auto'\n        },\n      }\n    : {}\n\n  // The closed book in its lying frame: x = cover width, y = thickness,\n  // z = spine length; resting on y = 0 with the spine along the -x edge.\n  const closed = (\n    <group>\n      <mesh castShadow position={[0.01 * unit, t / 2, 0]}>\n        <boxGeometry args={[w * 0.94, Math.max(t - board * 2, t * 0.5), h * 0.95]} />\n        <meshStandardMaterial color={pageColor} roughness={0.9} />\n      </mesh>\n      <mesh castShadow position={[0, board / 2, 0]}>\n        <boxGeometry args={[w, board, h]} />\n        <meshStandardMaterial color={coverColor} roughness={0.75} />\n      </mesh>\n      <mesh castShadow position={[0, t - board / 2, 0]}>\n        <boxGeometry args={[w, board, h]} />\n        <meshStandardMaterial color={coverColor} roughness={0.75} />\n      </mesh>\n      <mesh castShadow position={[-w / 2 + board / 2, t / 2, 0]}>\n        <boxGeometry args={[board, t, h]} />\n        <meshStandardMaterial color={coverColor} roughness={0.75} />\n      </mesh>\n    </group>\n  )\n\n  // One opened half: hinged at x = 0, extending toward +x; mirror with scaleX.\n  const openTilt = 0.14\n  const halfPages = Math.max((t - board * 2) / 2, t * 0.25)\n  const openHalf = (side: 1 | -1) => (\n    <group key={side} scale={[side, 1, 1]} rotation={[0, 0, -openTilt]}>\n      <mesh castShadow position={[w / 2, board / 2, 0]}>\n        <boxGeometry args={[w, board, h]} />\n        <meshStandardMaterial color={coverColor} roughness={0.75} />\n      </mesh>\n      <mesh castShadow position={[(w / 2) * 0.98, board + halfPages / 2, 0]}>\n        <boxGeometry args={[w * 0.92, halfPages, h * 0.94]} />\n        <meshStandardMaterial color={pageColor} roughness={0.9} />\n      </mesh>\n    </group>\n  )\n\n  const body =\n    pose === 'open' ? (\n      <group>{[1 as const, -1 as const].map(openHalf)}</group>\n    ) : pose === 'standing' ? (\n      // Lift by half the spine length, then tip the lying frame upright.\n      <group position={[0, h / 2, 0]} rotation={[-Math.PI / 2, 0, 0]}>\n        {closed}\n      </group>\n    ) : (\n      closed\n    )\n\n  const labelY =\n    pose === 'standing'\n      ? h + 0.1 * unit\n      : pose === 'open'\n        ? board + halfPages + 0.1 * unit\n        : t + 0.1 * unit\n\n  return (\n    <group position={position} rotation={rotation}>\n      <group scale={hovered ? 1.08 : 1} {...handlers}>\n        {body}\n      </group>\n      {hovered && title && (\n        <Html position={[0, labelY, 0]} center distanceFactor={6} zIndexRange={[30, 0]}>\n          <div\n            style={{\n              padding: '2px 8px',\n              borderRadius: 6,\n              background: 'rgba(10, 14, 20, 0.85)',\n              color: '#f4efe6',\n              font: '500 12px/1.4 system-ui, sans-serif',\n              whiteSpace: 'nowrap',\n              pointerEvents: 'none',\n            }}\n          >\n            {title}\n          </div>\n        </Html>\n      )}\n    </group>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
