{
  "name": "chandelier",
  "title": "Chandelier",
  "type": "registry:component",
  "description": "A hanging chandelier: a chain, a turned brass body, one or two rings of curved arms with candle bulbs, and optional glass drops. Instanced, with one shared light; can light only after dusk.",
  "dependencies": [
    "@runek/core@^0.15.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Chandelier.tsx",
      "content": "import { int, rng, sub, useSun, useWorld, type WorldComponentProps } from '@runek/core'\nimport { type ReactNode, useLayoutEffect, useMemo, useRef } from 'react'\nimport {\n  CubicBezierCurve3,\n  type InstancedMesh,\n  LatheGeometry,\n  Matrix4,\n  Quaternion,\n  TubeGeometry,\n  Vector2,\n  Vector3,\n} from 'three'\n\nexport interface ChandelierProps extends WorldComponentProps {\n  /** Arms on the lower tier. Seeded (5 to 8) when unset. */\n  arms?: number\n  /** One ring of arms, or two (the upper one smaller). Seeded when unset. */\n  tiers?: 1 | 2\n  /** Reach of the lower arms from the center, in units. */\n  radius?: number\n  /** Chain length from the ceiling to the body, in units. */\n  drop?: number\n  /** Glass drops hanging from the arms. Seeded when unset. */\n  crystals?: boolean\n  /** Frame, arms, and cups; defaults to the palette's `accent` (brass). */\n  color?: string\n  /** The light and the glowing bulb. */\n  lightColor?: string\n  /** Light strength. */\n  intensity?: number\n  /** Switched on. */\n  on?: boolean\n  /** Light only while the world's sun is down. */\n  nightOnly?: boolean\n}\n\ninterface Tier {\n  y: number\n  r: number\n  arms: number\n  offset: number\n}\n\n/** One draw call for every copy of a part: the matrices are set once per layout. */\nfunction Instanced({\n  matrices,\n  children,\n  castShadow,\n}: {\n  matrices: Matrix4[]\n  children: ReactNode\n  castShadow?: boolean\n}) {\n  const ref = useRef<InstancedMesh>(null)\n  useLayoutEffect(() => {\n    const mesh = ref.current\n    if (!mesh) return\n    matrices.forEach((m, i) => {\n      mesh.setMatrixAt(i, m)\n    })\n    mesh.instanceMatrix.needsUpdate = true\n  }, [matrices])\n  return (\n    <instancedMesh\n      key={matrices.length}\n      ref={ref}\n      args={[undefined, undefined, matrices.length]}\n      castShadow={castShadow}\n    >\n      {children}\n    </instancedMesh>\n  )\n}\n\nconst UP = new Vector3(0, 1, 0)\n\n/**\n * A hanging chandelier, its origin at the ceiling: a chain, a turned brass body, and one or two\n * rings of curved arms each ending in a cup and a candle bulb, with optional glass drops. Every\n * repeated part is instanced and the whole fitting shares one light, so a ballroom of them stays\n * cheap. `nightOnly` lights it with the world's dusk. Decorative: no collider.\n */\nexport function Chandelier({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  arms,\n  tiers,\n  radius = 0.45,\n  drop = 0.4,\n  crystals,\n  color,\n  lightColor = '#ffd9a0',\n  intensity = 22,\n  on = true,\n  nightOnly = false,\n}: ChandelierProps) {\n  const { unit, palette } = useWorld()\n  const sun = useSun()\n  const lit = on && (!nightOnly || !sun.day)\n  const brass = color ?? palette.accent\n  const R = radius * unit\n  const D = drop * unit\n  const bodyH = R * 0.75\n\n  const look = useMemo(() => {\n    const r = rng(sub(seed, 0))\n    const n = Math.max(3, Math.round(arms ?? int(r, 5, 8)))\n    const t = tiers ?? (r() < 0.45 ? 2 : 1)\n    const list: Tier[] = [{ y: -D - bodyH * 0.72, r: R, arms: n, offset: 0 }]\n    if (t === 2)\n      list.push({\n        y: -D - bodyH * 0.3,\n        r: R * 0.6,\n        arms: Math.max(3, Math.round(n * 0.6)),\n        offset: Math.PI / n,\n      })\n    return { tiers: list, crystals: crystals ?? r() < 0.6 }\n  }, [seed, arms, tiers, crystals, R, D, bodyH])\n\n  const parts = useMemo(() => {\n    const arm: Matrix4[] = []\n    const cup: Matrix4[] = []\n    const bulb: Matrix4[] = []\n    const drops: Matrix4[] = []\n    const q = new Quaternion()\n    const one = new Vector3(1, 1, 1)\n    for (const tier of look.tiers) {\n      for (let i = 0; i < tier.arms; i++) {\n        const a = tier.offset + (i / tier.arms) * Math.PI * 2\n        q.setFromAxisAngle(UP, -a)\n        const s = tier.r\n        arm.push(new Matrix4().compose(new Vector3(0, tier.y, 0), q, new Vector3(s, s, s)))\n        const tip = new Vector3(Math.cos(a) * s, tier.y + 0.12 * s + 0.02 * unit, Math.sin(a) * s)\n        cup.push(new Matrix4().compose(tip, q, one))\n        bulb.push(new Matrix4().compose(tip.clone().add(new Vector3(0, 0.07 * unit, 0)), q, one))\n        if (look.crystals) {\n          // under the arm's lowest point (the curve at t = 0.5)\n          const mid = new Vector3(\n            Math.cos(a) * s * 0.62,\n            tier.y - 0.27 * s - 0.045 * unit,\n            Math.sin(a) * s * 0.62,\n          )\n          drops.push(new Matrix4().compose(mid, q, one))\n        }\n      }\n    }\n    return { arm, cup, bulb, drops }\n  }, [look, unit])\n\n  // Arms are built at unit reach and scaled per tier: they dip below the body and rise to the cup.\n  const armGeometry = useMemo(() => {\n    const curve = new CubicBezierCurve3(\n      new Vector3(0.05, 0, 0),\n      new Vector3(0.35, -0.4, 0),\n      new Vector3(0.95, -0.35, 0),\n      new Vector3(1, 0.12, 0),\n    )\n    return new TubeGeometry(curve, 24, 0.022, 6, false)\n  }, [])\n\n  const bodyGeometry = useMemo(() => {\n    const k = bodyH\n    const profile = [\n      [0, 0],\n      [0.02, 0],\n      [0.03, 0.08],\n      [0.09, 0.2],\n      [0.12, 0.32],\n      [0.05, 0.45],\n      [0.04, 0.62],\n      [0.08, 0.75],\n      [0.03, 0.9],\n      [0.015, 1],\n      [0, 1],\n    ].map(([x, y]) => new Vector2(x * k * 1.2, -y * k))\n    return new LatheGeometry(profile, 20)\n  }, [bodyH])\n\n  const cupR = 0.035 * unit\n  const metal = <meshStandardMaterial color={brass} metalness={0.75} roughness={0.3} />\n\n  return (\n    <group position={position} rotation={rotation}>\n      <mesh position={[0, -0.015 * unit, 0]}>\n        <cylinderGeometry args={[0.05 * unit, 0.08 * unit, 0.03 * unit, 20]} />\n        {metal}\n      </mesh>\n      <mesh position={[0, -D / 2, 0]}>\n        <cylinderGeometry args={[0.006 * unit, 0.006 * unit, D, 6]} />\n        {metal}\n      </mesh>\n      <mesh position={[0, -D, 0]} geometry={bodyGeometry} castShadow>\n        {metal}\n      </mesh>\n      <Instanced matrices={parts.arm} castShadow>\n        <primitive object={armGeometry} attach=\"geometry\" />\n        {metal}\n      </Instanced>\n      <Instanced matrices={parts.cup}>\n        <cylinderGeometry args={[cupR, cupR * 0.4, 0.04 * unit, 12]} />\n        {metal}\n      </Instanced>\n      <Instanced matrices={parts.bulb}>\n        <sphereGeometry args={[0.022 * unit, 10, 8]} />\n        <meshStandardMaterial\n          color=\"#fff3d6\"\n          emissive={lightColor}\n          emissiveIntensity={lit ? 2.2 : 0}\n          toneMapped={!lit}\n        />\n      </Instanced>\n      {parts.drops.length > 0 && (\n        <Instanced matrices={parts.drops}>\n          <octahedronGeometry args={[0.03 * unit, 0]} />\n          <meshPhysicalMaterial\n            color=\"#e8f2ff\"\n            roughness={0.05}\n            transmission={0.6}\n            thickness={0.02 * unit}\n            emissive={lightColor}\n            emissiveIntensity={lit ? 0.25 : 0}\n          />\n        </Instanced>\n      )}\n      {lit && (\n        <pointLight\n          position={[0, look.tiers[0].y + 0.1 * R, 0]}\n          color={lightColor}\n          intensity={intensity}\n          distance={14 * unit}\n          decay={2}\n        />\n      )}\n    </group>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
