components/elements/SelectWidget/SelectWidget.jsxjavascript
import { useRef } from "react";
import { useEffect } from "react";
import { useElementSize } from "../../../hooks/elementSize";
import "./SelectWidget.scss";

/**
 * Circular story picker ("selectWidget" element type): options are spread
 * evenly around a ring; selecting one shows its title in the center and arms
 * the page's "next" navigation (it does not navigate by itself).
 *
 * @param {Object} props
 * @param {Array<{id: string, text: string, icon: string}>} [props.options] - Selectable options; `id` is a view id.
 * @param {"portrait"|"landscape"} [props.orientation="portrait"] - Ring/item sizing preset (from the content JSON).
 * @param {string|null} props.selectedId - Currently selected option id.
 * @param {function(string=): void} props.onSelect - Called with the tapped option's id, or with no argument on an outside click (deselects).
 */
export const SelectWidget = ({ options = [], orientation = "portrait", selectedId, onSelect }) => {
  const selectedOption = options.find((option) => option.id === selectedId);
  const btnRef = useRef([]);

  // Button positions are measured off the buttons-ring's own rendered size
  // rather than a hardcoded em constant - SelectWidget.scss's `--ring-size:
  // min(<em>, <N>cqmin)` can shrink that ring below its em value when space
  // is tight (e.g. the iPad Air's letterboxed portrait reference), and a
  // fixed em radius here would then place buttons outside the now-smaller
  // ring instead of on it. Measuring the actual box is the single source of
  // truth instead of duplicating that min() formula in JS.
  const [buttonsRingRef, { width: ringWidth, height: ringHeight }] = useElementSize();
  const ringDiameter = Math.min(ringWidth, ringHeight);

  // Figma spec's item diameter as a fraction of its buttons-ring diameter
  // (34.375/76.25 em portrait, 28/67 em landscape) rather than its own fixed
  // em value - same reasoning as ringDiameter above: an item sized in em
  // wouldn't shrink when the ring's `min(<em>, <N>cqmin)` cap shrinks it, so
  // buttons would end up too big for their own ring on a small enough
  // container (e.g. the iPad).
  const ITEM_DIAMETER_RATIO = orientation === "portrait" ? 34.375 / 76.25 : 28 / 67;
  const itemDiameter = ringDiameter * ITEM_DIAMETER_RATIO;

  useEffect(() => {
    const handleClickOutside = (e) => {
      const isClickOnAnyButton = btnRef.current.some((btn) => btn && btn.contains(e.target));

      if (!isClickOnAnyButton) {
        onSelect();
      }
    };
    // console.log(preview);
    document.addEventListener("click", handleClickOutside);
    return () => {
      document.removeEventListener("click", handleClickOutside);
    };
  }, [onSelect]);

  return (
    <div className="select-widget">
      <div className="select-widget__ring select-widget__ring--outer" />
      <div className="select-widget__ring select-widget__ring--dashed" />
      <div className="select-widget__ring select-widget__ring--buttons" ref={buttonsRingRef} />

      {selectedOption && <div className="select-widget__title">{selectedOption.text}</div>}

      {options.map((option, index) => {
        // spread options evenly around the button ring, starting at the top
        // (-90deg) and going clockwise
        const angle = -90 + index * (360 / options.length);
        const radians = (angle * Math.PI) / 180;
        const x = (ringDiameter / 2) * Math.cos(radians);
        const y = (ringDiameter / 2) * Math.sin(radians);

        return (
          <button
            ref={(el) => (btnRef.current[index] = el)}
            key={option.id}
            type="button"
            className={`select-widget__item${
              selectedId === option.id ? " select-widget__item--selected" : ""
            }`}
            style={{
              "--item-diameter": `${itemDiameter}px`,
              "--x": `${x}px`,
              "--y": `${y}px`,
            }}
            onClick={() => onSelect(option.id)}>
            {option.icon ? (
              <img className="select-widget__icon" src={option.icon} alt={option.text} />
            ) : (
              <span className="select-widget__label">{option.text}</span>
            )}
          </button>
        );
      })}
    </div>
  );
};