components/elements/IconWheel/IconWheel.jsxjavascript
import "./IconWheel.scss";

// ring radius in em, distributed the same way SelectWidget spreads its
// options: evenly around the circle, starting at the top (-90deg) clockwise
const NODE_RADIUS = 11;
const LABEL_RADIUS = 16.5;

/**
 * N icon nodes with text labels evenly spaced around a decorative ring
 * ("iconWheel" element type) - unlike CircleInfographic/SelectWidget, purely
 * decorative and non-interactive.
 *
 * @param {Object} props
 * @param {Array<{icon: string, label: string}>} props.items - Ring nodes.
 */
export const IconWheel = ({ items }) => {
  return (
    <div className="icon-wheel">
      <div className="icon-wheel__ring">
        {items.map((item, index) => {
          const angle = -90 + index * (360 / items.length);
          const radians = (angle * Math.PI) / 180;
          const cos = Math.cos(radians);
          const sin = Math.sin(radians);
          // side determines label text alignment: to the outside of the ring
          const side = cos > 0.3 ? "right" : cos < -0.3 ? "left" : "center";

          return (
            <div key={item.label} className="icon-wheel__spoke">
              <div
                className="icon-wheel__node"
                style={{ "--x": `${NODE_RADIUS * cos}em`, "--y": `${NODE_RADIUS * sin}em` }}>
                <img className="icon-wheel__icon" src={item.icon} alt="" />
              </div>
              <span
                className={`icon-wheel__label icon-wheel__label--${side}`}
                style={{ "--x": `${LABEL_RADIUS * cos}em`, "--y": `${LABEL_RADIUS * sin}em` }}>
                {item.label}
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
};