{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lighting-mast",
  "type": "registry:component",
  "title": "Lighting mast / pole",
  "description": "Area-lighting structure in two styles. Flood mast: tapered galvanised mast on a gusseted base plate with anchor bolts, inspection door and cable entry, lightning finial and a head frame carrying up to eight Ex floodlights on yokes (front and back rows) aimed down by `tilt`. Access is base-hinged (bolted flange as gravity lock, hinge pin, hydraulic counterbalance ram with hand pump; `lowered` swings the head down onto a rest for ground-level maintenance) or fixed with a caged ladder and fall-arrest rail. Pole: short galvanised pole with linear LED bulkhead fittings on arms and a base junction box. Lenses glow when `lit` (emissive only, no light sources).",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "electrical/lighting-mast.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/lighting-mast.tsx",
      "content": "// Floodlight mast or bulkhead lighting pole: tapered galvanised mast, base-hinged with hydraulic counterbalance (or fixed with caged ladder), Ex floodlights or LED fittings. EN 40, IEC 60079, IEC 60598.\nimport * as THREE from 'three'\nimport { useEffect, useMemo } from 'react'\nimport { Kit, eul, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { junctionBox, ladder, tagPlate } from '../lib/parts'\nimport { cableGland } from './_parts'\n\nexport interface LightingMastProps {\n  style?: 'flood-mast' | 'pole'\n  /** mast height above deck, m */\n  height?: number\n  /** number of floodlights / fittings */\n  count?: number\n  /** lamps on (emissive lenses) */\n  lit?: boolean\n  /** downward aim of the floodlights, degrees */\n  tilt?: number\n  /** flood mast: base-hinged (lowered for ground-level lamp maintenance) or fixed with a caged ladder */\n  access?: 'hinged' | 'ladder'\n  /** hinged mast lowered onto its rest */\n  lowered?: boolean\n  tag?: string\n}\n\nexport const defaults = {\n  style: 'flood-mast' as 'flood-mast' | 'pole',\n  height: 9,\n  count: 4,\n  lit: true,\n  tilt: 30,\n  access: 'hinged' as 'hinged' | 'ladder',\n  lowered: false,\n  tag: 'LM-101',\n}\n\nexport function LightingMast(props: LightingMastProps & GroupProps) {\n  const { style, height, count, lit, tilt, access, lowered, tag, ...group } = { ...defaults, ...props }\n  const b = useMemo(() => {\n    const k = new Kit()\n    const ku = new Kit()\n    const pivot = build(k, ku, { style, height, count, lit, tilt, access, lowered, tag })\n    return { base: k.build(), upper: ku.build(), pivot }\n  }, [style, height, count, lit, tilt, access, lowered, tag])\n  useEffect(() => () => [b.base, b.upper].forEach((x) => x.geometry.dispose()), [b])\n  const P = b.pivot\n  return (\n    <group {...group}>\n      <KitMesh data={b.base} />\n      <group position={P}>\n        <group rotation={[lowered && access === 'hinged' && style === 'flood-mast' ? -LOWER : 0, 0, 0]}>\n          <group position={[-P[0], -P[1], -P[2]]}>\n            <KitMesh data={b.upper} />\n          </group>\n        </group>\n      </group>\n    </group>\n  )\n}\n\nconst LOWER = (84 * Math.PI) / 180 // lowered angle, head resting on its cradle\n\nexport default LightingMast\n\nconst G: MatKey = 'galvanised'\n\nfunction base(k: Kit, r: number, plate: number) {\n  k.box(G, [plate, 0.025, plate], [0, 0.0125, 0])\n  for (let i = 0; i < 4; i++) {\n    const a = (i * Math.PI) / 2\n    const q = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -a)\n    const sh = new THREE.Shape()\n    const L = plate / 2 - r - 0.02\n    sh.moveTo(0, 0).lineTo(L, 0).lineTo(L, 0.03).lineTo(0, L * 1.3).closePath()\n    k.extrude(G, sh, 0.012, new THREE.Matrix4().compose(new THREE.Vector3(Math.cos(a) * r * 0.95, 0.025, -Math.sin(a) * r * 0.95), q, new THREE.Vector3(1, 1, 1)).multiply(new THREE.Matrix4().makeTranslation(0, 0, -0.006)))\n  }\n  const R = plate / 2 - 0.05\n  for (let i = 0; i < 8; i++) {\n    const a = ((i + 0.5) * Math.PI) / 4\n    const p: V3 = [Math.cos(a) * R * 1.05, 0, Math.sin(a) * R * 1.05]\n    k.cyl('boltXylan', 0.012, 0.09, [p[0], 0.06, p[2]], 'y', 8)\n    k.hex('nut', 0.036, 0.018, [p[0], 0.034, p[2]])\n    k.cyl('stainless', 0.024, 0.004, [p[0], 0.027, p[2]], 'y', 12)\n  }\n}\n\nfunction build(k: Kit, ku: Kit, p: typeof defaults): V3 {\n  const n = Math.max(1, Math.min(8, Math.round(p.count)))\n  const lens: MatKey = p.lit ? 'lampLens' : paint('#d8dcd8', { metalness: 0.1, roughness: 0.15, clearcoat: 0.6, weather: 0 })\n  const body = paint('#5d6468', { metalness: 0.5, roughness: 0.4, clearcoat: 0.2, weather: 0.4 })\n  if (p.style === 'pole') {\n    const H = Math.min(6, Math.max(2.5, p.height))\n    base(k, 0.045, 0.3)\n    k.cyl(G, 0.045, H, [0, H / 2, 0], 'y', 20, 0.038)\n    k.cyl(G, 0.042, 0.01, [0, H, 0], 'y', 16)\n    // fittings on arms, alternating sides, pairs stacked down the pole\n    for (let i = 0; i < n; i++) {\n      const s = i % 2 ? -1 : 1\n      const y = H - 0.15 - Math.floor(i / 2) * 0.6\n      k.rod(G, [0, y, 0], [s * 0.45, y + 0.05, 0], 0.022, 10)\n      k.box(G, [0.08, 0.06, 0.06], [s * 0.45, y + 0.02, 0])\n      const c: V3 = [s * 0.45, y - 0.06, 0]\n      k.rbox(body, [0.18, 0.09, 1.25], 0.02, c, undefined, 3)\n      k.box(lens, [0.13, 0.012, 1.15], [c[0], c[1] - 0.046, c[2]])\n      for (const dz of [-0.62, 0.62]) k.rbox('stainless', [0.16, 0.08, 0.02], 0.01, [c[0], c[1], dz])\n      cableGland(k, [c[0], c[1], -0.63], '-z', 0.015)\n    }\n    junctionBox(k, [0, 0.9, 0.045], 'z', [0.2, 0.22, 0.12])\n    k.spline('cable', [[0, 0.78, 0.1], [0, 0.4, 0.1], [0.05, 0.05, 0.25], [0.3, 0.015, 0.35]], 0.009, 6, 16)\n    tagPlate(k, p.tag, [0, 1.35, 0.041], 'z', 0.1)\n    return [0, 0, 0]\n  }\n\n  const H = Math.min(25, Math.max(5, p.height))\n  const r0 = 0.1 + H * 0.004\n  const r1 = 0.06\n  const rAt = (y: number) => r0 + (r1 - r0) * (y / H)\n  base(k, r0, 0.55)\n  const hinged = p.access === 'hinged'\n  const yh = 1.2\n  const ru = rAt(yh)\n  // base section up to the joint, inspection door with termination box behind, cable entry\n  k.cyl(G, r0, yh, [0, yh / 2 + 0.025, 0], 'y', 28, ru)\n  k.box(G, [0.12, 0.38, 0.03], [0, 0.6, rAt(0.6) - 0.005])\n  for (const y of [0.45, 0.75]) k.hex('nut', 0.02, 0.012, [0, y, rAt(0.6) + 0.012], 'z')\n  k.spline('cable', [[0, 0.3, rAt(0.3) + 0.02], [0, 0.12, rAt(0.12) + 0.06], [0.05, 0.03, 0.45], [0.3, 0.015, 0.7]], 0.012, 6, 16)\n  k.cyl('brass', 0.022, 0.05, [0, 0.33, rAt(0.33) + 0.01], 'y', 10)\n  // flanged joint: lower flange on the base, upper flange on the mast; bolts are the gravity lock when raised\n  k.cyl(G, ru + 0.06, 0.02, [0, yh + 0.015, 0], 'y', 28)\n  ku.cyl(G, ru + 0.06, 0.02, [0, yh + 0.035, 0], 'y', 28)\n  ku.boltCircle('boltXylan', 'nut', 8, ru + 0.035, [0, yh + 0.005, 0], 'y', 0.014, 0.06, [0.0, 0.052])\n  ku.cyl(G, ru, H - yh, [0, yh + (H - yh) / 2 + 0.045, 0], 'y', 28, r1)\n  const pivot: V3 = [0, yh + 0.025, -ru - 0.07]\n  if (hinged) {\n    // hinge knuckles (base side) and pin, lug on the mast side\n    for (const s of [-1, 1]) k.box(G, [0.025, 0.16, 0.12], [s * 0.06, yh - 0.04, -ru - 0.05])\n    k.cyl('stainless', 0.022, 0.16, pivot, 'x', 12)\n    ku.box(G, [0.08, 0.14, 0.1], [0, yh + 0.08, -ru - 0.05])\n    // hydraulic counterbalance ram: barrel on a base bracket, rod to a mast bracket\n    const a: V3 = [0, 0.18, -r0 - 0.45]\n    k.box(G, [0.16, 0.18, 0.2], [0, 0.11, -r0 - 0.45])\n    k.box(G, [0.16, 0.012, 0.5], [0, 0.031, -r0 - 0.25])\n    const lowered = p.lowered\n    const bLocal = new THREE.Vector3(0, yh + 1.6, -rAt(yh + 1.6) - 0.12)\n    ku.box(G, [0.1, 0.16, 0.14], [0, yh + 1.6, -rAt(yh + 1.6) - 0.06])\n    const bw = lowered ? bLocal.clone().sub(new THREE.Vector3(...pivot)).applyAxisAngle(new THREE.Vector3(1, 0, 0), -LOWER).add(new THREE.Vector3(...pivot)) : bLocal\n    const A = new THREE.Vector3(...a)\n    const d = bw.clone().sub(A)\n    const mid = A.clone().addScaledVector(d, 0.55)\n    k.rod(paint('#2f3b4a', { metalness: 0.4, roughness: 0.4 }), a, mid.toArray() as V3, 0.045, 14)\n    k.rod('stainless', mid.toArray() as V3, bw.toArray() as V3, 0.022, 10)\n    k.cyl('nut', 0.03, 0.12, a, 'x', 10)\n    // hand pump / control valve block and mast rest (cradle) at the lowered head position\n    k.rbox(paint('#c0261c', { roughness: 0.4 }), [0.18, 0.22, 0.12], 0.015, [0.35, 0.14, -r0 - 0.45])\n    if (lowered) {\n      // portable mast rest under the head while lowered\n      const Lr = H - yh - 0.6\n      const zr = pivot[2] - Lr * Math.sin(LOWER)\n      const yTop = pivot[1] + Lr * Math.cos(LOWER) - ru * 0.6 - 0.02\n      k.rod(G, [0, 0.02, zr], [0, yTop - 0.06, zr], 0.05, 12)\n      k.box(G, [0.4, 0.02, 0.4], [0, 0.01, zr])\n      k.box(G, [0.24, 0.02, 0.1], [0, yTop - 0.06, zr])\n      for (const sx of [-1, 1]) k.box(G, [0.02, 0.14, 0.1], [sx * (ru + 0.02), yTop, zr])\n    }\n  } else {\n    // fixed mast: caged ladder (ISO 14122-4) with a fall-arrest rail (EN 353-1) for lamp access\n    ladder(ku, [0, 0.025, -rAt(0) - 0.25], H - 0.6, [0, 0, -1], { mat: G, extend: 0 })\n    ku.box('stainless', [0.03, H - 0.8, 0.012], [0, (H - 0.8) / 2 + 0.6, -rAt(0) - 0.25])\n    for (let y = 1.5; y < H - 0.6; y += 2) ku.rod(G, [0, y, -rAt(y)], [0, y, -rAt(y) - 0.25], 0.012, 6)\n  }\n  // head frame: one cross-arm per row of up to four lamps, front row faces +Z, back row -Z\n  const rows = n > 4 ? 2 : 1\n  const yA = H - 0.25\n  const tilt = (Math.max(0, Math.min(80, p.tilt)) * Math.PI) / 180\n  ku.cyl(G, r1 + 0.02, 0.02, [0, H + 0.035, 0], 'y', 20)\n  ku.rod(G, [0, H + 0.045, 0], [0, H + 0.6, 0], 0.012, 8) // lightning finial\n  for (let rI = 0; rI < rows; rI++) {\n    const s = rI === 0 ? 1 : -1\n    const m = rI === 0 ? Math.min(4, n) : n - 4\n    const span = Math.max(0.6, m * 0.55)\n    const za = s * (r1 + 0.06)\n    ku.box(G, [span, 0.08, 0.08], [0, yA, za])\n    ku.box(G, [0.16, 0.2, 0.012], [0, yA, s * (r1 + 0.01)])\n    for (let i = 0; i < m; i++) {\n      const x = -span / 2 + (span * (i + 0.5)) / m\n      // yoke and floodlight aimed down by `tilt`\n      for (const dx of [-0.25, 0.25]) ku.box(G, [0.015, 0.2, 0.04], [x + dx, yA - 0.12, za + s * 0.06])\n      ku.box(G, [0.52, 0.015, 0.04], [x, yA - 0.03, za + s * 0.06])\n      const c: V3 = [x, yA - 0.2, za + s * 0.1]\n      const q = eul(s * tilt, s > 0 ? 0 : Math.PI, 0)\n      ku.at(c, q, () => {\n        ku.rbox(body, [0.46, 0.36, 0.12], 0.02, [0, 0, -0.02], undefined, 3)\n        for (let f = 0; f < 6; f++) ku.box(body, [0.42, 0.3, 0.008], [0, 0, -0.09 - f * 0.014], undefined)\n        ku.box('stainless', [0.44, 0.34, 0.012], [0, 0, 0.045])\n        ku.box(lens, [0.4, 0.3, 0.008], [0, 0, 0.053])\n        ku.box(body, [0.46, 0.04, 0.1], [0, 0.19, 0.03]) // visor\n      })\n      ku.spline('cable', [[x + 0.1, yA - 0.04, za], [x + 0.12, yA - 0.2, za + s * 0.02], [c[0] + 0.12, c[1], c[2] - s * 0.1]], 0.007, 6, 10)\n    }\n  }\n  tagPlate(k, p.tag, [0, 0.95, rAt(0.95) + 0.002], 'z', 0.12)\n  return pivot\n}\n"
    },
    {
      "path": "electrical/_parts.ts",
      "type": "registry:file",
      "target": "components/deck-twin/electrical/_parts.ts",
      "content": "// Shared electrical details: profiled cladding sheet, cable glands, earth bosses, louvres, multi-cable transits, beacons.\nimport * as THREE from 'three'\nimport { eul, type Kit, type MatKey, type V3 } from '../lib/kit'\nimport { louvreMaterial } from '../lib/textures'\n\n/** Trapezoidal (vertical-rib) profiled sheet: x ∈ [-w/2, w/2], y ∈ [0, h], ribs stand out to +Z by `depth`. */\nexport function ribbedPanel(w: number, h: number, pitch = 0.25, depth = 0.035, back = true): THREE.BufferGeometry {\n  const n = Math.max(1, Math.round(w / pitch))\n  const p = w / n\n  const prof: [number, number][] = [[0, 0], [0.3, 0], [0.4, depth], [0.6, depth], [0.7, 0], [1, 0]]\n  const xs: [number, number][] = []\n  for (let i = 0; i < n; i++)\n    for (let j = 0; j < prof.length - (i < n - 1 ? 1 : 0); j++) {\n      if (i > 0 && j === 0) continue\n      xs.push([-w / 2 + (i + prof[j][0]) * p, prof[j][1]])\n    }\n  const pos: number[] = []\n  const nor: number[] = []\n  const uv: number[] = []\n  const push = (q: number[][], nx: number, nz: number) => {\n    for (const v of q) {\n      pos.push(v[0], v[1], v[2])\n      nor.push(nx, 0, nz)\n      uv.push((v[0] + w / 2) / w, v[1] / h)\n    }\n  }\n  for (let i = 0; i < xs.length - 1; i++) {\n    const [x0, z0] = xs[i]\n    const [x1, z1] = xs[i + 1]\n    const l = Math.hypot(x1 - x0, z1 - z0) || 1\n    push([[x0, 0, z0], [x1, 0, z1], [x1, h, z1], [x0, 0, z0], [x1, h, z1], [x0, h, z0]], -(z1 - z0) / l, (x1 - x0) / l)\n  }\n  if (back) push([[w / 2, 0, -0.002], [-w / 2, 0, -0.002], [-w / 2, h, -0.002], [w / 2, 0, -0.002], [-w / 2, h, -0.002], [w / 2, h, -0.002]], 0, -1)\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3))\n  g.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3))\n  g.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2))\n  return g\n}\n\n/** Ex e / Ex d cable gland with a shroud, axis along `axis` (gland body from pos outwards), cable size by OD. */\nexport function cableGland(k: Kit, pos: V3, axis: 'y' | '-y' | 'x' | '-x' | 'z' | '-z', cableOD = 0.025) {\n  const d: V3 = axis === 'y' ? [0, 1, 0] : axis === '-y' ? [0, -1, 0] : axis === 'x' ? [1, 0, 0] : axis === '-x' ? [-1, 0, 0] : axis === 'z' ? [0, 0, 1] : [0, 0, -1]\n  const at = (t: number): V3 => [pos[0] + d[0] * t, pos[1] + d[1] * t, pos[2] + d[2] * t]\n  const r = cableOD / 2\n  k.hex('brass', r * 3.4, 0.012, at(0.006), axis)\n  k.cyl('brass', r * 1.55, 0.03, at(0.027), axis, 12)\n  k.hex('brass', r * 3.0, 0.014, at(0.049), axis)\n  k.cyl('rubberBlack', r * 1.45, 0.035, at(0.073), axis, 12, r * 1.1)\n  return at(0.09)\n}\n\n/** Brass earthing boss on a face (normal axis) with a bolted green/yellow bond lug. */\nexport function earthBoss(k: Kit, pos: V3, axis: 'x' | '-x' | 'z' | '-z') {\n  const s = axis.startsWith('-') ? -1 : 1\n  const off = (t: number): V3 => (axis.endsWith('x') ? [pos[0] + s * t, pos[1], pos[2]] : [pos[0], pos[1], pos[2] + s * t])\n  k.cyl('brass', 0.022, 0.03, off(0.015), axis, 12)\n  k.hex('nut', 0.028, 0.012, off(0.036), axis)\n  k.box('copper', axis.endsWith('x') ? [0.006, 0.05, 0.02] : [0.02, 0.05, 0.006], [off(0.032)[0], pos[1] - 0.025, off(0.032)[2]])\n}\n\n/** Weather louvre in a wall frame (outer face at z = 0, +Z out): frame, angled blades, bird mesh behind. */\nexport function louvre(k: Kit, u: number, y: number, w: number, h: number, frame: MatKey = 'structSteelDark', blade: MatKey = 'cladding') {\n  k.box(frame, [w + 0.08, 0.04, 0.08], [u, y + h / 2 + 0.02, 0.03])\n  k.box(frame, [w + 0.08, 0.04, 0.08], [u, y - h / 2 - 0.02, 0.03])\n  for (const s of [-1, 1]) k.box(frame, [0.04, h + 0.08, 0.08], [u + s * (w / 2 + 0.02), y, 0.03])\n  k.box('rubberBlack', [w, h, 0.01], [u, y, 0.004])\n  const n = Math.max(3, Math.round(h / 0.07))\n  for (let i = 0; i < n; i++) k.box(blade, [w, 0.075, 0.006], [u, y - h / 2 + (i + 0.5) * (h / n), 0.04], eul(0.7, 0, 0))\n}\n\n/** Flat louvre panel (texture only), facing +normal, for small enclosures. */\nexport function louvrePanel(k: Kit, w: number, h: number, pos: V3, q: THREE.Quaternion, color = '#c9ccc8') {\n  const g = new THREE.PlaneGeometry(w, h)\n  const uv = g.getAttribute('uv') as THREE.BufferAttribute\n  for (let i = 0; i < uv.count; i++) uv.setY(i, uv.getY(i) * Math.max(1, h / 0.08))\n  k.add(louvreMaterial(color), g, new THREE.Matrix4().compose(new THREE.Vector3(...pos), q, new THREE.Vector3(1, 1, 1)))\n}\n\n/** Multi-cable transit frame (MCT) on a wall face at z = 0, with packed rubber modules. */\nexport function cableTransit(k: Kit, u: number, y: number, cols = 6, rows = 2, frame: MatKey = 'structSteelDark') {\n  const w = cols * 0.075 + 0.08\n  const h = rows * 0.09 + 0.05\n  k.box(frame, [w, h, 0.12], [u, y, 0.06])\n  for (let r = 0; r < rows; r++)\n    for (let c = 0; c < cols; c++) k.box(c % 2 === r % 2 ? 'rubberBlack' : 'gasket', [0.07, 0.08, 0.02], [u - ((cols - 1) * 0.075) / 2 + c * 0.075, y - ((rows - 1) * 0.09) / 2 + r * 0.09, 0.125])\n  k.box('stainless', [w - 0.04, 0.02, 0.03], [u, y + h / 2 - 0.01, 0.125])\n  return { w, h }\n}\n\n/** Status beacon (sounder base + coloured lens), standing on +Y at pos. */\nexport function beacon(k: Kit, pos: V3, lens: MatKey) {\n  k.cyl('plasticGrey', 0.06, 0.06, [pos[0], pos[1] + 0.03, pos[2]], 'y', 14)\n  k.cyl(lens, 0.05, 0.11, [pos[0], pos[1] + 0.115, pos[2]], 'y', 16)\n  k.dome(lens, 0.05, [pos[0], pos[1] + 0.17, pos[2]], 'y', Math.PI / 2, 0.6, 16)\n}\n"
    }
  ],
  "categories": [
    "electrical"
  ],
  "meta": {
    "export": "LightingMast",
    "anchor": "base",
    "standards": [
      "EN 40-3",
      "IEC 60598-2-5",
      "IEC 60079-0 / -1 / -7",
      "ISO 14122-4"
    ]
  }
}