tsx
import { useRef } from "react";
import {
  motion,
  useMotionValue,
  useTransform,
  useSpring,
  useTime,
  useReducedMotion,
  type MotionValue,
} from "framer-motion";

// Ambient breathing dot grid with a cursor force field.
//
// Dots pulse outward from the center in a continuous radial wave. The
// cursor only affects POSITION: moving over the grid REPELS the dots,
// holding the button ATTRACTS them (a gravity well), and each dot
// springs back to its home.
//
// Breathing is independent of the cursor — there's no global hover
// pause. A dot only "calms" (its pulse eases down) in proportion to
// how far it's actually displaced: calm = displacement / MAX_DISP.
// Because calm rides the displacement spring, it eases smoothly and
// per-dot, so nothing flashes when the cursor enters or leaves.
//
// Tuning: FALLOFF (reach), MAX_DISP (throw distance), SPRING (feel),
// PERIOD/SCALE_AMP/OPACITY_AMP (the breath). Cursor state lives in
// motion values, so moving the mouse never triggers a React re-render.

type DotProps = {
  homeX: number;
  homeY: number;
  phase: number;
  reduced: boolean;
  time: MotionValue<number>;
  cursorX: MotionValue<number>;
  cursorY: MotionValue<number>;
  active: MotionValue<number>;
  attract: MotionValue<number>;
};

function Dot({
  homeX,
  homeY,
  phase,
  reduced,
  time,
  cursorX,
  cursorY,
  active,
  attract,
}: DotProps) {
  // Force field → displacement. Read every shared value up front so the
  // transform subscribes to all of them, no matter which branch returns.
  const dispX = useTransform(() => {
    const a = active.get();
    const cx = cursorX.get();
    const cy = cursorY.get();
    const repel = attract.get() === 0;
    if (a === 0) return 0;
    return offset(homeX, homeY, cx, cy, repel)[0];
  });
  const dispY = useTransform(() => {
    const a = active.get();
    const cx = cursorX.get();
    const cy = cursorY.get();
    const repel = attract.get() === 0;
    if (a === 0) return 0;
    return offset(homeX, homeY, cx, cy, repel)[1];
  });
  const x = useSpring(dispX, SPRING);
  const y = useSpring(dispY, SPRING);

  // Calm tracks the field intensity at the dot's home: 1 right under the
  // cursor, easing to 0 at the falloff radius (and 0 when the cursor is
  // away). Its own damped spring eases it in/out — smooth and per-dot,
  // with no global hover state, so nothing flashes on enter/leave.
  const intensity = useTransform(() => {
    const a = active.get();
    const cx = cursorX.get();
    const cy = cursorY.get();
    if (a === 0) return 0;
    const dist = Math.hypot(homeX - cx, homeY - cy);
    if (dist > FALLOFF) return 0;
    const t = dist / FALLOFF;
    return (1 - t) * (1 - t);
  });
  const calm = useSpring(intensity, CALM_SPRING);

  // Continuous breathing pulse (raised cosine, 0..1) with a radial phase
  // offset. Its amplitude is attenuated by `calm`. x/y (translate),
  // scale, and opacity are independent style channels, so displacement
  // and breathing compose without fighting.
  const pulse = useTransform(() => {
    if (reduced) return 0;
    const tSec = time.get() / 1000;
    return (1 - Math.cos((2 * Math.PI) / PERIOD * (tSec - phase))) / 2;
  });
  const scale = useTransform(
    () => 1 + SCALE_AMP * pulse.get() * (1 - calm.get())
  );
  const opacity = useTransform(
    () => BASE_OPACITY + OPACITY_AMP * pulse.get() * (1 - calm.get())
  );

  return (
    <motion.div
      style={{
        x,
        y,
        scale,
        opacity,
        width: DOT_SIZE,
        height: DOT_SIZE,
        borderRadius: "50%",
        background: "currentColor",
        pointerEvents: "none", // the wrapper handles all pointer events
      }}
    />
  );
}

export default function BreathingDots() {
  const gridRef = useRef<HTMLDivElement>(null);
  const time = useTime();
  const cursorX = useMotionValue(0); // grid-local px
  const cursorY = useMotionValue(0);
  const active = useMotionValue(0); // 0 = cursor outside, 1 = inside (gates the force)
  const attract = useMotionValue(0); // 0 = repel (default), 1 = attract (button held)
  const reduced = useReducedMotion() ?? false;

  function handleMove(e: React.PointerEvent) {
    if (reduced) return;
    const grid = gridRef.current;
    if (!grid) return;
    const r = grid.getBoundingClientRect();
    cursorX.set(e.clientX - r.left);
    cursorY.set(e.clientY - r.top);
  }

  function engage() {
    if (reduced) return;
    active.set(1);
  }

  function release() {
    active.set(0);
    attract.set(0);
  }

  function press() {
    if (reduced) return;
    attract.set(1);
  }

  function lift() {
    attract.set(0);
  }

  const dots = [];
  for (let row = 0; row < GRID_SIZE; row++) {
    for (let col = 0; col < GRID_SIZE; col++) {
      const distance = Math.sqrt(
        Math.pow(row - GRID_SIZE / 2, 2) + Math.pow(col - GRID_SIZE / 2, 2)
      );
      dots.push(
        <Dot
          key={`${row}-${col}`}
          homeX={col * PITCH + DOT_SIZE / 2}
          homeY={row * PITCH + DOT_SIZE / 2}
          phase={distance * WAVE}
          reduced={reduced}
          time={time}
          cursorX={cursorX}
          cursorY={cursorY}
          active={active}
          attract={attract}
        />
      );
    }
  }

  return (
    // Padded wrapper = invisible hit area so the field engages before the
    // cursor reaches the tight grid box. Coordinates are measured off the
    // inner grid, so the padding doesn't affect the math.
    <div
      onPointerMove={handleMove}
      onPointerEnter={engage}
      onPointerLeave={release}
      onPointerDown={press}
      onPointerUp={lift}
      onPointerCancel={release}
      onDragStart={(e) => e.preventDefault()}
      style={{
        padding: PAD,
        display: "inline-block",
        touchAction: "none",
        userSelect: "none",
        WebkitTapHighlightColor: "transparent",
      }}
    >
      <div
        ref={gridRef}
        style={{
          display: "grid",
          gridTemplateColumns: `repeat(${GRID_SIZE}, ${DOT_SIZE}px)`,
          gridTemplateRows: `repeat(${GRID_SIZE}, ${DOT_SIZE}px)`,
          gap: GAP,
          color: "var(--foreground)",
        }}
      >
        {dots}
      </div>
    </div>
  );
}

// Displacement of a dot from its home, given the cursor position.
// `repel` true pushes the dot away from the cursor; false pulls it in.
function offset(
  homeX: number,
  homeY: number,
  cursorX: number,
  cursorY: number,
  repel: boolean
): [number, number] {
  const dx = homeX - cursorX; // vector pointing from cursor → dot
  const dy = homeY - cursorY;
  const dist = Math.hypot(dx, dy) || 0.0001;
  if (dist > FALLOFF) return [0, 0];
  const t = dist / FALLOFF;
  const falloff = (1 - t) * (1 - t); // strong core, soft edge
  const amount = Math.min(MAX_DISP, MAX_DISP * falloff) * (repel ? 1 : -1);
  return [(dx / dist) * amount, (dy / dist) * amount];
}

// Layout
const GRID_SIZE = 8;
const DOT_SIZE = 6;
const GAP = 28;
const PITCH = DOT_SIZE + GAP; // distance between dot centers

// Breathing pulse
const PERIOD = 3; // seconds per breath
const WAVE = 0.15; // phase offset per unit of grid distance (radial wave)
const SCALE_AMP = 0.8; // scale ranges 1 .. 1 + SCALE_AMP
const BASE_OPACITY = 0.25;
const OPACITY_AMP = 0.35; // opacity ranges BASE .. BASE + AMP

// Cursor force field
const FALLOFF = 120; // px radius of influence
const MAX_DISP = 26; // px max displacement (~0.75 × pitch — crowd, don't overlap)
const PAD = 100; // invisible hit-area padding around the grid
const SPRING = { stiffness: 220, damping: 18, mass: 0.9 }; // displacement — underdamped → elastic
const CALM_SPRING = { stiffness: 150, damping: 30 }; // calm — overdamped → ease, no overshoot
Breathing dots