{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "monorail-hoist",
  "type": "registry:component",
  "title": "Monorail with chain hoist",
  "description": "Runway beam (IPE or HEB, sized by SWL) hung from HEB goalpost supports at ≤ 6 m with base plates, anchor bolts and knee braces, end stops with rubber buffers and SWL marking. A push trolley with four flanged wheels on the bottom flange carries an electric chain hoist with motor, gearbox, chain bag and push-button pendant; round-link load chain to the bottom hook. trolley (0–1) moves it along X and hook sets the chain drop.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "lifting/monorail-hoist.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/lifting/monorail-hoist.tsx",
      "content": "// Monorail runway beam on goalpost supports with an electric chain hoist on a push trolley (EN 14492-2 / FEM 9.511, ISO 4301 runway).\nimport * as THREE from 'three'\nimport { useMemo } from 'react'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { paint } from '../lib/materials'\nimport { member, tagPlate } from '../lib/parts'\nimport { SECTIONS } from '../lib/standards'\nimport { textMaterial } from '../lib/textures'\nimport { boxMM, craneYellow, hook } from './_rigging'\n\nexport interface MonorailHoistProps {\n  /** runway beam length, m */\n  length?: number\n  /** underside of the runway beam above the deck, m */\n  height?: number\n  /** hoist safe working load, t */\n  swl?: number\n  /** trolley position along the beam, 0 … 1 */\n  trolley?: number\n  /** hook below the hoist body, m */\n  hook?: number\n  tag?: string\n}\n\nexport const defaults = {\n  length: 8,\n  height: 4.5,\n  swl: 5,\n  trolley: 0.4,\n  hook: 2.2,\n  tag: 'HM-101',\n}\n\nconst clamp = (v: number, a: number, b: number) => Math.max(a, Math.min(b, v))\n\nfunction dims(length: number, height: number, swl: number) {\n  const beam = swl <= 2 ? 'IPE 240' : swl <= 5 ? 'IPE 330' : swl <= 10 ? 'HEB 400' : 'HEB 500'\n  const sec = SECTIONS[beam]\n  const s = clamp(0.8 + swl / 20, 0.8, 1.6)\n  const L = Math.max(3, length)\n  const H = Math.max(2.5, height)\n  const rW = 0.065 * s\n  const yShaft = H - 0.13 * s\n  const yBody = yShaft - 0.12 * s\n  const bodyH = 0.42 * s\n  return { beam, sec, s, L, H, rW, yShaft, yBody, bodyH, chainY: yBody - bodyH, stop: 0.35, d: clamp(0.007 + swl * 0.0018, 0.007, 0.024) }\n}\n\nexport function MonorailHoist({ length = defaults.length, height = defaults.height, swl = defaults.swl, trolley = defaults.trolley, hook: drop = defaults.hook, tag = defaults.tag, ...group }: MonorailHoistProps & GroupProps) {\n  const D = useMemo(() => dims(length, height, swl), [length, height, swl])\n  const runway = useKit((k) => buildRunway(k, D, swl, tag), [D, swl, tag])\n  const carriage = useKit((k) => buildHoist(k, D), [D])\n  const hookDrop = clamp(drop, 0.3, D.chainY - 0.6)\n  const chain = useKit((k) => buildChain(k, D, hookDrop), [D, hookDrop])\n  const span = D.L / 2 - D.stop - 0.35 * D.s\n  const x = -span + 2 * span * clamp(trolley, 0, 1)\n  return (\n    <group {...group}>\n      <KitMesh data={runway} />\n      <group position={[x, 0, 0]}>\n        <KitMesh data={carriage} />\n        <KitMesh data={chain} />\n      </group>\n    </group>\n  )\n}\n\nexport default MonorailHoist\n\ntype D = ReturnType<typeof dims>\n\nfunction buildRunway(k: Kit, D: D, swl: number, tag: string) {\n  const { L, H, sec } = D\n  const Y = craneYellow()\n  const steel: MatKey = 'structSteel'\n  // runway beam (bottom flange underside at H)\n  member(k, [-L / 2, H + sec.d / 2, 0], [L / 2, H + sec.d / 2, 0], D.beam, Y)\n  // goalpost supports ≤ 6 m apart\n  const col = 'HEB 200'\n  const cs = SECTIONS[col]\n  const half = Math.max(1.0, 0.6 + D.s * 0.4)\n  const yHdr = H + sec.d + 0.03 + cs.d / 2\n  const n = Math.max(2, Math.ceil((L - 0.6) / 6) + 1)\n  for (let i = 0; i < n; i++) {\n    const x = -L / 2 + 0.3 + ((L - 0.6) * i) / (n - 1)\n    member(k, [x, yHdr, -half - cs.b / 2], [x, yHdr, half + cs.b / 2], col, steel)\n    boxMM(k, steel, [x - sec.b / 2 - 0.03, H + sec.d, -0.18], [x + sec.b / 2 + 0.03, H + sec.d + 0.03, 0.18])\n    for (const dx of [-1, 1]) for (const dz of [-0.13, 0.13]) k.hex('nut', 0.036, 0.02, [x + dx * (sec.b / 2 + 0.005), H + sec.d - sec.tf - 0.01, dz])\n    for (const sz of [-1, 1]) {\n      const z = sz * half\n      member(k, [x, 0.025, z], [x, yHdr + cs.d / 2, z], col, steel, [1, 0, 0])\n      boxMM(k, steel, [x - 0.2, 0, z - 0.2], [x + 0.2, 0.025, z + 0.2])\n      for (const dx of [-0.14, 0.14]) for (const dz of [-0.14, 0.14]) {\n        k.cyl('boltXylan', 0.012, 0.08, [x + dx, 0.05, z + dz], 'y', 8)\n        k.hex('nut', 0.04, 0.022, [x + dx, 0.036, z + dz])\n      }\n      k.rod(steel, [x, yHdr - 0.65, z], [x, yHdr - cs.d / 2, z - sz * 0.6], 0.04, 8)\n    }\n  }\n  // end stops with rubber buffers, SWL marking both sides\n  for (const sx of [-1, 1]) {\n    const xs = sx * (L / 2 - D.stop)\n    for (const sz of [-1, 1]) {\n      boxMM(k, Y, [xs - 0.02, H - 0.12, sz * (sec.b / 2 + 0.002) - 0.008], [xs + 0.02, H + sec.tf + 0.08, sz * (sec.b / 2 + 0.002) + 0.008])\n      k.cyl('rubberBlack', 0.045, 0.08, [xs - sx * 0.06, H - 0.05, sz * (sec.b / 2 - 0.02)], 'x', 14)\n    }\n    boxMM(k, Y, [xs - 0.02, H - 0.12, -sec.b / 2 - 0.01], [xs + 0.02, H - 0.09, sec.b / 2 + 0.01])\n  }\n  const sign = textMaterial(`SWL ${swl} t`, '#111111', '#f2c40f', 512, 128)\n  for (const sz of [-1, 1]) {\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), sz > 0 ? 0 : Math.PI)\n    k.plane(sign, Math.min(0.9, sec.d * 3.2), Math.min(0.9, sec.d * 3.2) / 4, [0, H + sec.d / 2, sz * (sec.tw / 2 + 0.003)], q)\n    if (tag) tagPlate(k, tag, [-L / 2 + 0.6, H + sec.d / 2, sz * (sec.tw / 2 + 0.001)], [0, 0, sz], Math.min(0.3, sec.d))\n  }\n}\n\n// push trolley on the bottom flange with an electric chain hoist and control pendant\nfunction buildHoist(k: Kit, D: D) {\n  const { H, sec, s, rW } = D\n  const body = paint('#e1b10f', { weather: 0.5 })\n  const dark: MatKey = 'castIron'\n  const zw = sec.tw / 2 + 0.035 * s\n  const yW = H + sec.tf + rW\n  const zp = sec.b / 2 + 0.03\n  for (const sz of [-1, 1]) {\n    boxMM(k, dark, [-0.22 * s, D.yShaft - 0.07 * s, sz * zp - 0.008], [0.22 * s, yW + rW + 0.02, sz * zp + 0.008])\n    for (const sx of [-1, 1]) {\n      k.cyl(dark, rW, 0.035 * s, [sx * 0.13 * s, yW, sz * zw], 'z', 18)\n      k.cyl('stainless', 0.015, zp - zw, [sx * 0.13 * s, yW, sz * (zw + zp) / 2], 'z', 8)\n      k.hex('nut', 0.04, 0.02, [sx * 0.13 * s, yW, sz * (zp + 0.012)], 'z')\n    }\n  }\n  k.cyl('stainless', 0.022 * s, 2 * zp + 0.06, [0, D.yShaft, 0], 'z', 10)\n  for (const sz of [-1, 1]) k.hex('nut', 0.06 * s, 0.03, [0, D.yShaft, sz * (zp + 0.03)], 'z')\n  // suspension lug, hoist body, motor, gearbox, contactor box, chain bag\n  boxMM(k, dark, [-0.03 * s, D.yBody - 0.01, -0.025 * s], [0.03 * s, D.yShaft + 0.04 * s, 0.025 * s])\n  k.rbox(body, [0.32 * s, D.bodyH, 0.3 * s], 0.03 * s, [0, D.yBody - D.bodyH / 2, 0])\n  k.cyl(body, 0.12 * s, 0.32 * s, [0, D.yBody - D.bodyH * 0.45, 0.31 * s], 'z', 24)\n  for (let i = 0; i < 10; i++) k.cyl(body, 0.13 * s, 0.006, [0, D.yBody - D.bodyH * 0.45, 0.2 * s + i * 0.022 * s], 'z', 24)\n  k.cyl('rubberBlack', 0.1 * s, 0.03, [0, D.yBody - D.bodyH * 0.45, 0.48 * s], 'z', 20)\n  k.rbox(body, [0.26 * s, 0.3 * s, 0.14 * s], 0.025 * s, [0, D.yBody - D.bodyH * 0.45, -0.22 * s])\n  k.rbox('plasticGrey', [0.18 * s, 0.14 * s, 0.08 * s], 0.01, [0.0, D.yBody - D.bodyH * 0.2, -0.33 * s])\n  k.plane(textMaterial('EN 14492-2', '#f1f1ec', '#111111', 256, 64), 0.12 * s, 0.03 * s, [0, D.yBody - D.bodyH * 0.6, 0.151 * s])\n  k.cyl('rubberBlack', 0.09 * s, 0.32 * s, [0.12 * s, D.chainY - 0.18 * s, 0], 'y', 16, 0.07 * s)\n  // pendant: strain wire + cable down to the push-button station\n  const yp = Math.max(1.1, D.chainY - 1.2)\n  k.spline('cable', [[-0.1 * s, D.yBody - D.bodyH * 0.5, -0.12 * s], [-0.2 * s, (D.yBody + yp) / 2, -0.15 * s], [-0.24 * s, yp + 0.22, -0.16 * s]], 0.008, 6, 24)\n  k.rbox('plasticGrey', [0.08, 0.26, 0.07], 0.02, [-0.24 * s, yp + 0.08, -0.16 * s])\n  for (const [dy, m] of [[0.15, 'safetyGreen'], [0.09, 'actuatorRed'], [0.03, 'equipmentPaint']] as [number, MatKey][]) k.cyl(m, 0.016, 0.012, [-0.24 * s, yp + dy, -0.16 * s + 0.036], 'z', 12)\n  k.cyl('actuatorRed', 0.022, 0.02, [-0.24 * s, yp + 0.23, -0.16 * s], 'y', 12)\n}\n\n// Round-link load chain (alternating links) to the bottom hook block.\nfunction buildChain(k: Kit, D: D, drop: number) {\n  const d = D.d\n  const p = 3 * d\n  const n = Math.max(2, Math.round(drop / p))\n  const y0 = D.chainY\n  const x = -0.03 * D.s\n  for (let i = 0; i < n; i++) {\n    const g = new THREE.TorusGeometry(1.15 * d, d / 2, 6, 12)\n    g.scale(1, 1.55, 1)\n    if (i % 2) g.rotateY(Math.PI / 2)\n    k.add('galvanised', g, new THREE.Matrix4().makeTranslation(x, y0 - (i + 0.5) * p, 0))\n  }\n  const yb = y0 - n * p\n  const s = D.s\n  k.rbox(craneYellow(), [0.11 * s, 0.16 * s, 0.13 * s], 0.02 * s, [x, yb - 0.08 * s, 0])\n  k.cyl('castIron', 0.03 * s, 0.06 * s, [x, yb - 0.19 * s, 0], 'y', 12)\n  hook(k, [x, yb - 0.3 * s, 0], 0.045 * s * (0.8 + D.d * 20), 'castIron')\n}\n"
    },
    {
      "path": "lifting/_rigging.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/lifting/_rigging.tsx",
      "content": "// Shared rigging for the lifting category: wire rope, shackles, master links, 4-leg sling sets, hooks, hazard panels and oriented bars.\nimport * as THREE from 'three'\nimport { useMemo } from 'react'\nimport { Kit, mat4, uvScale, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { liftingLug } from '../lib/parts'\nimport { hazardStripeMaterial } from '../lib/textures'\n\nconst UNIT_ROPE = new THREE.CylinderGeometry(1, 1, 1, 8, 1, false)\n\n/** Galvanised wire rope (dark, oily). */\nexport const ropeMaterial = () => paint('#3a3d40', { metalness: 0.75, roughness: 0.42, clearcoat: 0, weather: 0 })\n/** Offshore crane / lifting-gear yellow (RAL 1003). */\nexport const craneYellow = () => paint('#eab308', { weather: 0.9 })\n\n/** Axis-aligned box from min to max corners. */\nexport function boxMM(k: Kit, mat: MatKey, a: V3, b: V3) {\n  k.box(mat, [Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]), Math.abs(b[2] - a[2])], [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2])\n}\n\n/** Rectangular bar from a to b, w across (perpendicular to up), h along `up`. */\nexport function bar(k: Kit, mat: MatKey, a: V3, b: V3, w: number, h: number, up: V3 = [0, 1, 0]) {\n  const A = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(A)\n  const L = d.length()\n  if (L < 1e-6) return\n  const x = d.normalize()\n  let u = new THREE.Vector3(...up).normalize()\n  if (Math.abs(u.dot(x)) > 0.99) u = Math.abs(x.y) > 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0)\n  const z = new THREE.Vector3().crossVectors(x, u).normalize()\n  const y = new THREE.Vector3().crossVectors(z, x).normalize()\n  const m = new THREE.Matrix4().makeBasis(x, y, z).setPosition(A.addScaledVector(x, L / 2))\n  k.add(mat, new THREE.BoxGeometry(L, h, w), m)\n}\n\n/** Black/yellow hazard chevron panel of w × h, centred at pos, facing `normal`, laid 4 mm proud. */\nexport function hazardPanel(k: Kit, pos: V3, w: number, h: number, normal: V3, stripe = 0.3) {\n  const n = new THREE.Vector3(...normal).normalize()\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), n)\n  const g = new THREE.PlaneGeometry(w, h)\n  uvScale(g, w / (stripe * 2), h / (stripe * 2))\n  const p = new THREE.Vector3(...pos).addScaledVector(n, 0.004)\n  k.add(hazardStripeMaterial(), g, mat4(p.toArray() as V3, q))\n}\n\n/** Bow shackle: bow and pin in the plane of `along` and +Y, pin centre at pos (pairs with a padeye plate across `along`). s = jaw width. */\nexport function shackle(k: Kit, pos: V3, s = 0.06, along: 'x' | 'z' = 'x', mat: MatKey = 'galvanised') {\n  const r = s * 0.22\n  const q = along === 'x' ? new THREE.Quaternion() : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2)\n  const m = (p: V3) => new THREE.Matrix4().compose(new THREE.Vector3(...p).applyQuaternion(q).add(new THREE.Vector3(...pos)), q, new THREE.Vector3(1, 1, 1))\n  const bow = new THREE.TorusGeometry(s / 2, r, 6, 16, Math.PI)\n  k.add(mat, bow, m([0, s * 0.9, 0]))\n  for (const sx of [-1, 1]) k.add(mat, new THREE.CylinderGeometry(r, r, s * 0.9, 6), m([sx * (s / 2), s * 0.45, 0]))\n  k.add('nut', new THREE.CylinderGeometry(r * 0.95, r * 0.95, s + r * 6, 8).rotateZ(Math.PI / 2), m([0, 0, 0]))\n  for (const sx of [-1, 1]) k.add(mat, new THREE.SphereGeometry(r * 1.25, 8, 6), m([sx * (s / 2), 0, 0]))\n}\n\n/** Top of a shackle's bow (where a sling eye sits). */\nexport const shackleTop = (pos: V3, s = 0.06): V3 => [pos[0], pos[1] + s * 1.4, pos[2]]\n\n/** Oblong master link, long axis vertical, centre at pos. */\nexport function masterLink(k: Kit, pos: V3, s = 0.25, mat: MatKey = 'galvanised') {\n  const g = new THREE.TorusGeometry(s * 0.32, s * 0.08, 8, 24)\n  g.scale(1, 1.55, 1)\n  k.add(mat, g, mat4(pos))\n}\n\n/** Four-leg sling set: legs from each attachment point to a master link at `apex`, with thimble eyes and ferrules. */\nexport function slingSet(k: Kit, eyes: V3[], apex: V3, s = 0.25, rope = 0.012) {\n  const rm = ropeMaterial()\n  const link = s * 0.32 * 1.55\n  const bottom: V3 = [apex[0], apex[1] - link + s * 0.04, apex[2]]\n  masterLink(k, apex, s)\n  // intermediate links (one per pair of legs)\n  for (const sx of [-1, 1]) {\n    const g = new THREE.TorusGeometry(s * 0.16, s * 0.05, 6, 18)\n    g.scale(1, 1.4, 1)\n    k.add('galvanised', g, mat4([bottom[0] + sx * s * 0.12, bottom[1] - s * 0.12, bottom[2]], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2)))\n  }\n  const hang: V3 = [bottom[0], bottom[1] - s * 0.3, bottom[2]]\n  for (const e of eyes) {\n    const top: V3 = [hang[0] + Math.sign(e[0] - hang[0]) * s * 0.12, hang[1], hang[2] + Math.sign(e[2] - hang[2]) * s * 0.02]\n    const E = new THREE.Vector3(...e)\n    const T = new THREE.Vector3(...top)\n    const dir = T.clone().sub(E).normalize()\n    // thimble eye at the shackle, ferrule a little up the leg\n    const eyeC = E.clone().addScaledVector(dir, 0.06)\n    k.rod(rm, eyeC.toArray() as V3, T.toArray() as V3, rope, 6)\n    k.rod('aluminium', eyeC.clone().addScaledVector(dir, 0.12).toArray() as V3, eyeC.clone().addScaledVector(dir, 0.22).toArray() as V3, rope * 1.9, 8)\n    k.torus('galvanised', 0.035, rope * 0.9, eyeC.toArray() as V3, new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), new THREE.Vector3(-dir.z, 0, dir.x).normalize()), 6, 14)\n    k.rod(rm, T.toArray() as V3, [top[0], top[1] + 0.04, top[2]], rope, 6)\n  }\n}\n\n/** Forged single hook (DIN 15401 shape): shank up from pos, bowl below, latch. size = bowl inner radius. */\nexport function hook(k: Kit, pos: V3, size = 0.12, mat: MatKey = 'castIron', latch: MatKey = 'safetyYellow') {\n  const [x, y, z] = pos\n  const r = size\n  const t = size * 0.42\n  k.cyl(mat, t * 0.55, r * 1.3, [x, y + r * 0.65, z], 'y', 12)\n  k.hex('nut', t * 1.6, t * 0.6, [x, y + r * 1.35, z])\n  // bowl: 300° arc of a tapered section, open towards +X\n  const arc = new THREE.TorusGeometry(r, t * 0.6, 10, 24, (5 * Math.PI) / 3)\n  arc.rotateZ(Math.PI / 2 + Math.PI / 6)\n  k.add(mat, arc, mat4([x, y - r * 1.0, z], undefined, [1, 1.05, 1.3]))\n  k.sphere(mat, t * 0.55, [x + r * Math.cos(Math.PI / 6 + Math.PI / 2 + (5 * Math.PI) / 3), y - r + r * Math.sin(Math.PI / 6 + Math.PI / 2 + (5 * Math.PI) / 3), z], undefined, 10)\n  k.rod(latch, [x + t * 0.5, y + r * 0.1, z], [x + r * 0.95, y - r * 0.45, z], t * 0.12, 6)\n}\n\n/** Stretched-rope mesh between two points (moves with props without rebuilding geometry). */\nexport function Rope({ a, b, r = 0.022, material }: { a: V3; b: V3; r?: number; material?: THREE.Material }) {\n  const t = useMemo(() => {\n    const A = new THREE.Vector3(...a)\n    const d = new THREE.Vector3(...b).sub(A)\n    const L = Math.max(d.length(), 1e-4)\n    const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), d.normalize())\n    return { p: A.addScaledVector(d, L / 2), q, s: new THREE.Vector3(r, L, r) }\n  }, [a[0], a[1], a[2], b[0], b[1], b[2], r]) // eslint-disable-line react-hooks/exhaustive-deps\n  const m = material ?? ropeMaterial()\n  return <mesh geometry={UNIT_ROPE} material={m} position={t.p} quaternion={t.q} scale={t.s} castShadow />\n}\n\n/** Padeye plate (across `across`) with a bow shackle through its hole; returns the bow top for the sling eye. */\nexport function padeyeShackle(k: Kit, pos: V3, across: 'x' | 'z' = 'x', s = 1, mat: MatKey = 'structSteel'): V3 {\n  liftingLug(k, pos, across === 'x' ? 'z' : 'x', s, mat)\n  const pin: V3 = [pos[0], pos[1] + 0.12 * s * 0.65, pos[2]]\n  shackle(k, pin, 0.065 * s, across)\n  return shackleTop(pin, 0.065 * s)\n}\n"
    }
  ],
  "categories": [
    "lifting"
  ],
  "meta": {
    "export": "MonorailHoist",
    "anchor": "base",
    "standards": [
      "EN 14492-2",
      "FEM 9.511",
      "ISO 4301-1",
      "EN 818-7"
    ]
  }
}