shadimage
beui
blockavailability-schedulersource: ok · hit

Availability Scheduler

Weekly availability editor where each day springs between available and unavailable, time ranges add and remove with blur-slide motion, times pick from a scrollable dropdown, and a copy menu clones hours to other days.

npx shadcn add "https://beui.dev/r/availability-scheduler.json"
free

Visual style

glass-blur

Interaction

dropdown

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
iconslucide-react
animationmotion

Files22 · source cached 10/15/2026, 6:15:27 AM

components/motion/availability-scheduler/index.tsx
"use client";
// beui.dev/components/blocks/availability-scheduler

import { LayoutGroup, useReducedMotion } from "motion/react";
import {
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from "react";
import { cn } from "@/lib/utils";
import { DayRow } from "./day-row";
import {
  buildOptions,
  type DayAvailability,
  type DayKey,
  defaultWeek,
  panelKey,
  WEEKDAYS,
  type WeekAvailability,
} from "./types";

export type {
  DayAvailability,
  DayKey,
  TimeRange,
  WeekAvailability,
} from "./types";
export { defaultWeek } from "./types";

export interface AvailabilitySchedulerProps {
  value?: WeekAvailability;
  defaultValue?: WeekAvailability;
  onChange?: (value: WeekAvailability) => void;
  /** Minutes between selectable times. Default 30. */
  step?: number;
  className?: string;
}

export function AvailabilityScheduler({
  value,
  defaultValue,
  onChange,
  step = 30,
  className,
}: AvailabilitySchedulerProps) {
  const reduce = useReducedMotion() ?? false;
  const groupId = useId();
  const options = useMemo(() => buildOptions(step), [step]);
  const idRef = useRef(0);

  const [internal, setInternal] = useState<WeekAvailability>(
    () => defaultValue ?? defaultWeek(),
  );
  // The row that last opened a dropdown paints above the rest — see DayRow.
  const [openDay, setOpenDay] = useState<DayKey | null>(null);
  // Exactly one time panel is open at a time, and the scheduler is the one that
  // knows which. A panel is absolutely positioned inside its own field, so two
  // open at once paint over each other's options — and nothing else can close
  // the first: a Select only dismisses on an outside *pointerdown*, which
  // keyboard and assistive-technology activation never fires.
  const [openPanel, setOpenPanel] = useState<string | null>(null);
  const controlled = value !== undefined;
  const week = controlled ? value : internal;

  // Which panels the week currently puts on screen. A field that leaves — its
  // day switched off, its range removed — never reports its panel closed: the
  // only thing that dismisses a controlled Select is an outside pointerdown,
  // and a field on its way out is no longer there to hear one. Keeping its id
  // would reopen the panel the moment the same range came back.
  const livePanels = useMemo(() => {
    const ids = new Set<string>();
    for (const { key } of WEEKDAYS) {
      if (!week[key].enabled) continue;
      for (const range of week[key].ranges) {
        ids.add(panelKey(key, range.id, "start"));
        ids.add(panelKey(key, range.id, "end"));
      }
    }
    return ids;
  }, [week]);

  useEffect(() => {
    if (openPanel !== null && !livePanels.has(openPanel)) setOpenPanel(null);
  }, [livePanels, openPanel]);

  const commit = useCallback(
    (next: WeekAvailability) => {
      if (!controlled) setInternal(next);
      onChange?.(next);
    },
    [controlled, onChange],
  );

  const setDay = useCallback(
    (day: DayKey, next: DayAvailability) => {
      commit({ ...week, [day]: next });
    },
    [commit, week],
  );

  const panelOpenChange = useCallback(
    (day: DayKey, id: string, open: boolean) => {
      setOpenPanel((current) =>
        open ? id : current === id ? null : current,
      );
      // Elevation stays on the row that opened last so the panel's collapse
      // animation finishes above its neighbours.
      if (open) setOpenDay(day);
    },
    [],
  );

  const copyDay = useCallback(
    (from: DayKey, targets: DayKey[]) => {
      const source = week[from];
      const next = { ...week };
      for (const t of targets) {
        next[t] = {
          enabled: source.enabled,
          ranges: source.ranges.map((r) => ({
            ...r,
            id: `${t}-c${idRef.current++}`,
          })),
        };
      }
      commit(next);
    },
    [commit, week],
  );

  return (
    <LayoutGroup id={groupId}>
      <div className={cn("w-full max-w-xl divide-y divide-border", className)}>
        {WEEKDAYS.map(({ key, label }) => (
          <DayRow
            key={key}
            day={key}
            label={label}
            state={week[key]}
            options={options}
            reduce={reduce}
            elevated={openDay === key}
            openPanel={openPanel}
            onChange={(next) => setDay(key, next)}
            onCopy={(targets) => copyDay(key, targets)}
            onPanelOpenChange={(id, open) => panelOpenChange(key, id, open)}
          />
        ))}
      </div>
    </LayoutGroup>
  );
}
components/motion/availability-scheduler/day-row.tsx
"use client";

import { Plus, X } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useRef, useState } from "react";
import { Switch } from "@/components/motion/switch";
import { Tooltip } from "@/components/motion/tooltip";
import { SPRING_LAYOUT } from "@/lib/ease";
import { CopyMenu } from "./copy-menu";
import { IconButton } from "./icon-button";
import { TimeSelect } from "./time-select";
import {
  clampRange,
  type DayAvailability,
  type DayKey,
  endOptions,
  panelKey,
  startOptions,
  type TimeOption,
  type TimeRange,
  toMinutes,
  toValue,
} from "./types";

export function DayRow({
  day,
  label,
  state,
  options,
  reduce,
  elevated,
  openPanel,
  onChange,
  onCopy,
  onPanelOpenChange,
}: {
  day: DayKey;
  label: string;
  state: DayAvailability;
  options: TimeOption[];
  reduce: boolean;
  // True while this row holds the dropdown that opened last, which paints it
  // above every other row. A time panel opens downward when there is room and
  // upward when there isn't, so it has to clear the rows on either side of it
  // — no fixed paint order can satisfy both directions. The flag stays on
  // after the panel closes so the collapse animation stays on top too.
  elevated: boolean;
  /** Id of the one time panel the scheduler is holding open, if any. */
  openPanel: string | null;
  onChange: (next: DayAvailability) => void;
  onCopy: (targets: DayKey[]) => void;
  onPanelOpenChange: (panelId: string, open: boolean) => void;
}) {
  const idRef = useRef(0);
  const nextId = () => `${day}-n${idRef.current++}`;
  // Same rule one level down: ranges stack against each other inside the row.
  const [openRangeId, setOpenRangeId] = useState<string | null>(null);

  const panelId = (rangeId: string, edge: "start" | "end") =>
    panelKey(day, rangeId, edge);

  const onRangePanelOpenChange = (
    rangeId: string,
    id: string,
    open: boolean,
  ) => {
    if (open) setOpenRangeId(rangeId);
    onPanelOpenChange(id, open);
  };

  const setEnabled = (enabled: boolean) => {
    if (enabled && state.ranges.length === 0) {
      onChange({
        enabled,
        ranges: [{ id: nextId(), start: "09:00", end: "17:00" }],
      });
    } else {
      onChange({ ...state, enabled });
    }
  };

  const updateRange = (id: string, patch: Partial<TimeRange>) => {
    const changed: "start" | "end" = patch.start !== undefined ? "start" : "end";
    onChange({
      ...state,
      ranges: state.ranges.map((r) => {
        if (r.id !== id) return r;
        const next = { ...r, ...patch };
        return { ...next, ...clampRange(next.start, next.end, options, changed) };
      }),
    });
  };

  const addRange = () => {
    const last = state.ranges[state.ranges.length - 1];
    const start = last ? Math.min(toMinutes(last.end) + 60, 24 * 60 - 60) : 540;
    onChange({
      enabled: true,
      ranges: [
        ...state.ranges,
        { id: nextId(), start: toValue(start), end: toValue(start + 60) },
      ],
    });
  };

  const removeRange = (id: string) => {
    const ranges = state.ranges.filter((r) => r.id !== id);
    // Removing the last slot marks the day unavailable.
    onChange({ enabled: ranges.length > 0, ranges });
  };

  const actions = (
    <>
      <Tooltip content="Add time">
        <IconButton
          label={`Add time range to ${label}`}
          reduce={reduce}
          onClick={addRange}
        >
          <Plus className="h-4 w-4" />
        </IconButton>
      </Tooltip>
      <CopyMenu fromLabel={label} reduce={reduce} onApply={onCopy} />
    </>
  );

  return (
    <motion.div
      layout={reduce ? false : "position"}
      transition={SPRING_LAYOUT}
      style={{ zIndex: elevated ? 1 : undefined }}
      className="relative flex flex-col gap-3 py-4 sm:flex-row sm:items-start sm:gap-4"
    >
      {/* toggle + label; actions ride along on mobile */}
      <div className="flex items-center justify-between sm:w-36 sm:shrink-0 sm:justify-start sm:pt-1">
        <div className="flex items-center gap-2.5">
          <Switch
            checked={state.enabled}
            onCheckedChange={setEnabled}
            ariaLabel={`Toggle ${label} availability`}
            className="scale-90"
          />
          <span className="text-sm font-medium text-foreground">{label}</span>
        </div>
        <div className="flex items-center gap-1 sm:hidden">{actions}</div>
      </div>

      {/* ranges or unavailable */}
      <div className="flex min-w-0 flex-1 flex-col gap-2">
        <AnimatePresence initial={false} mode="popLayout">
          {state.enabled ? (
            state.ranges.map((r) => (
              <motion.div
                key={r.id}
                layout={reduce ? false : "position"}
                style={{ zIndex: openRangeId === r.id ? 1 : undefined }}
                initial={
                  reduce
                    ? { opacity: 0 }
                    : { opacity: 0, y: -6, filter: "blur(4px)" }
                }
                animate={
                  reduce
                    ? { opacity: 1 }
                    : { opacity: 1, y: 0, filter: "blur(0px)" }
                }
                exit={
                  reduce
                    ? { opacity: 0 }
                    : { opacity: 0, y: -4, filter: "blur(4px)" }
                }
                transition={SPRING_LAYOUT}
                className="relative flex items-center gap-2"
              >
                <div className="min-w-0 flex-1 sm:max-w-[132px]">
                  <TimeSelect
                    value={r.start}
                    options={startOptions(options, r.end, r.start)}
                    onChange={(v) => updateRange(r.id, { start: v })}
                    open={openPanel === panelId(r.id, "start")}
                    onOpenChange={(open) =>
                      onRangePanelOpenChange(r.id, panelId(r.id, "start"), open)
                    }
                  />
                </div>
                <span className="text-muted-foreground">–</span>
                <div className="min-w-0 flex-1 sm:max-w-[132px]">
                  <TimeSelect
                    value={r.end}
                    options={endOptions(options, r.start, r.end)}
                    onChange={(v) => updateRange(r.id, { end: v })}
                    open={openPanel === panelId(r.id, "end")}
                    onOpenChange={(open) =>
                      onRangePanelOpenChange(r.id, panelId(r.id, "end"), open)
                    }
                  />
                </div>
                <Tooltip content="Remove">
                  <IconButton
                    label="Remove time range"
                    reduce={reduce}
                    onClick={() => removeRange(r.id)}
                  >
                    <X className="h-4 w-4" />
                  </IconButton>
                </Tooltip>
              </motion.div>
            ))
          ) : (
            <motion.span
              key="unavailable"
              layout={reduce ? false : "position"}
              initial={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}
              animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0 }}
              exit={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}
              transition={SPRING_LAYOUT}
              className="py-1 text-sm text-muted-foreground sm:py-2"
            >
              Unavailable
            </motion.span>
          )}
        </AnimatePresence>
      </div>

      {/* actions (desktop) */}
      <div className="hidden shrink-0 items-center gap-1 pt-0.5 sm:flex">
        {actions}
      </div>
    </motion.div>
  );
}
components/motion/availability-scheduler/types.ts
export type DayKey = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";

export type TimeRange = { id: string; start: string; end: string };
export type DayAvailability = { enabled: boolean; ranges: TimeRange[] };
export type WeekAvailability = Record<DayKey, DayAvailability>;
export type TimeOption = { value: string; label: string };

export const WEEKDAYS: { key: DayKey; label: string }[] = [
  { key: "mon", label: "Monday" },
  { key: "tue", label: "Tuesday" },
  { key: "wed", label: "Wednesday" },
  { key: "thu", label: "Thursday" },
  { key: "fri", label: "Friday" },
  { key: "sat", label: "Saturday" },
  { key: "sun", label: "Sunday" },
];

// ─── panels ──────────────────────────────────────────────────────────────────

/**
 * Names one time field week-wide, because the scheduler holds a single open
 * panel for the whole week. Range ids belong to the value and are only unique
 * within a day, so the day is part of the name — and the scheduler builds the
 * same keys to ask whether the panel it is holding is still on screen.
 */
export const panelKey = (day: DayKey, rangeId: string, edge: "start" | "end") =>
  `${day}:${rangeId}:${edge}`;

// ─── time helpers ────────────────────────────────────────────────────────────

export const toMinutes = (v: string) => {
  const [h, m] = v.split(":").map(Number);
  return h * 60 + m;
};

export const toValue = (mins: number) => {
  const clamped = Math.max(0, Math.min(24 * 60 - 1, mins));
  const h = Math.floor(clamped / 60);
  const m = clamped % 60;
  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
};

export const label12 = (v: string) => {
  const [h, m] = v.split(":").map(Number);
  const ap = h < 12 ? "AM" : "PM";
  const h12 = h % 12 === 0 ? 12 : h % 12;
  return `${h12}:${String(m).padStart(2, "0")} ${ap}`;
};

export function buildOptions(step: number): TimeOption[] {
  const out: TimeOption[] = [];
  for (let m = 0; m < 24 * 60; m += step) {
    const value = toValue(m);
    out.push({ value, label: label12(value) });
  }
  return out;
}

function snapToOption(mins: number, slots: number[]) {
  let best = slots[0];
  for (const slot of slots) {
    if (Math.abs(slot - mins) < Math.abs(best - mins)) best = slot;
  }
  return best;
}

function withCurrentOption(
  filtered: TimeOption[],
  all: TimeOption[],
  current?: string,
) {
  if (!current || filtered.some((o) => o.value === current)) return filtered;
  const extra =
    all.find((o) => o.value === current) ??
    ({ value: current, label: label12(current) } satisfies TimeOption);
  return [...filtered, extra].sort(
    (a, b) => toMinutes(a.value) - toMinutes(b.value),
  );
}

/**
 * Same-day ranges only. A valid pair is left alone so an off-grid persisted
 * end (17:00 with `step={720}`) is not rewritten. When the invariant fails,
 * the just-selected endpoint stays and only the opposite side moves onto a
 * neighboring generated option.
 */
export function clampRange(
  start: string,
  end: string,
  options: TimeOption[],
  changed: "start" | "end" = "start",
): { start: string; end: string } {
  const slots = options.map((o) => toMinutes(o.value));
  if (slots.length === 0) return { start, end };

  const startM = toMinutes(start);
  const endM = toMinutes(end);
  if (endM > startM) return { start, end };

  const keepOrSnap = (value: string) => {
    const mins = toMinutes(value);
    return slots.includes(mins) ? mins : snapToOption(mins, slots);
  };

  if (changed === "end") {
    const e = keepOrSnap(end);
    const earlier = [...slots].reverse().find((slot) => slot < e);
    if (earlier === undefined && slots.length > 1) {
      // Midnight cannot end a positive same-day range, so use the first pair.
      return { start: toValue(slots[0]), end: toValue(slots[1]) };
    }
    return {
      start: toValue(earlier ?? keepOrSnap(start)),
      end: toValue(e),
    };
  }

  const s = keepOrSnap(start);
  const later = slots.find((slot) => slot > s);
  if (later === undefined && slots.length > 1) {
    // The last slot cannot start a positive range, so use the final pair.
    return {
      start: toValue(slots[slots.length - 2]),
      end: toValue(slots[slots.length - 1]),
    };
  }
  return {
    start: toValue(s),
    end: toValue(later ?? keepOrSnap(end)),
  };
}

export function startOptions(
  options: TimeOption[],
  end: string,
  current?: string,
) {
  const filtered = options.filter(
    (o) => toMinutes(o.value) < toMinutes(end),
  );
  // An invalid midnight end has no earlier option; expose midnight so choosing
  // it can move the end forward through clampRange instead of trapping the row.
  const recovery = filtered.length === 0 ? options.slice(0, 1) : filtered;
  return withCurrentOption(
    recovery,
    options,
    current,
  );
}

export function endOptions(
  options: TimeOption[],
  start: string,
  current?: string,
) {
  const filtered = options.filter(
    (o) => toMinutes(o.value) > toMinutes(start),
  );
  // An invalid last-slot start has no later option; expose the last slot so
  // choosing it can move the start backward through clampRange.
  const recovery = filtered.length === 0 ? options.slice(-1) : filtered;
  return withCurrentOption(
    recovery,
    options,
    current,
  );
}

// Default: Mon–Fri 9–5, weekend off. Fixed ids so SSR and first client render
// agree (new ranges get counter ids afterwards).
export function defaultWeek(): WeekAvailability {
  const workday = (day: DayKey): DayAvailability => ({
    enabled: true,
    ranges: [{ id: `${day}-0`, start: "09:00", end: "17:00" }],
  });
  const off = (day: DayKey): DayAvailability => ({
    enabled: false,
    ranges: [{ id: `${day}-0`, start: "09:00", end: "17:00" }],
  });
  return {
    mon: workday("mon"),
    tue: workday("tue"),
    wed: workday("wed"),
    thu: workday("thu"),
    fri: workday("fri"),
    sat: off("sat"),
    sun: off("sun"),
  };
}
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
components/motion/availability-scheduler/copy-menu.tsx
"use client";

import { Check, Copy } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { Checkbox } from "@/components/motion/checkbox";
import {
  MorphPopover,
  MorphPopoverContent,
} from "@/components/motion/popover-morph";
import { Tooltip } from "@/components/motion/tooltip";
import { SPRING_PRESS } from "@/lib/ease";
import { IconButton } from "./icon-button";
import { type DayKey, WEEKDAYS } from "./types";

// Copy this day's hours to other days: a morph popover with a day picker.
export function CopyMenu({
  fromLabel,
  reduce,
  onApply,
}: {
  fromLabel: string;
  reduce: boolean;
  onApply: (targets: DayKey[]) => void;
}) {
  const [open, setOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const [picked, setPicked] = useState<Set<DayKey>>(new Set());
  const others = WEEKDAYS.filter((d) => d.label !== fromLabel);

  const toggle = (k: DayKey) =>
    setPicked((prev) => {
      const next = new Set(prev);
      if (next.has(k)) next.delete(k);
      else next.add(k);
      return next;
    });

  const apply = (targets: DayKey[]) => {
    if (!targets.length) return;
    onApply(targets);
    setOpen(false);
    setPicked(new Set());
    setCopied(true);
    window.setTimeout(() => setCopied(false), 1200);
  };

  return (
    <MorphPopover open={open} onOpenChange={setOpen}>
      <Tooltip content="Copy times">
        <IconButton
          label={`Copy ${fromLabel} hours to other days`}
          reduce={reduce}
          expanded={open}
          onClick={() => setOpen(!open)}
        >
          <AnimatePresence mode="popLayout" initial={false}>
            {copied ? (
              <motion.span
                key="done"
                initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}
                transition={SPRING_PRESS}
                className="text-foreground"
              >
                <Check className="h-4 w-4" />
              </motion.span>
            ) : (
              <motion.span
                key="copy"
                initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}
                transition={SPRING_PRESS}
              >
                <Copy className="h-4 w-4" />
              </motion.span>
            )}
          </AnimatePresence>
        </IconButton>
      </Tooltip>

      <MorphPopoverContent align="end" className="w-52 p-2">
        <p className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
          Copy times to
        </p>
        <div className="flex flex-col">
          {others.map((d) => (
            <Checkbox
              key={d.key}
              checked={picked.has(d.key)}
              onCheckedChange={() => toggle(d.key)}
              label={d.label}
              className="w-full flex-row-reverse justify-between rounded-lg px-2 py-1.5 transition-colors hover:bg-muted [&_button]:size-4 [&_button]:rounded-[5px] [&_button]:border [&_button[data-state=unchecked]]:border-border-strong"
            />
          ))}
        </div>
        <div className="mt-1 flex items-center gap-2 border-t border-border px-1 pt-2">
          <button
            type="button"
            onClick={() => apply(others.map((d) => d.key))}
            className="flex-1 rounded-lg px-2 py-1.5 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:bg-muted"
          >
            Every day
          </button>
          <button
            type="button"
            onClick={() => apply([...picked])}
            disabled={picked.size === 0}
            className="flex-1 rounded-lg bg-primary px-2 py-1.5 text-xs font-semibold text-primary-foreground outline-none transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40"
          >
            Apply
          </button>
        </div>
      </MorphPopoverContent>
    </MorphPopover>
  );
}
components/motion/availability-scheduler/icon-button.tsx
"use client";

import { motion } from "motion/react";
import type { ReactNode } from "react";
import { SPRING_PRESS } from "@/lib/ease";
import { cn } from "@/lib/utils";

export function IconButton({
  onClick,
  label,
  disabled,
  expanded,
  reduce,
  children,
  className,
  // Rest props let a wrapping Tooltip inject its hover/focus handlers.
  ...rest
}: {
  onClick: () => void;
  label: string;
  disabled?: boolean;
  expanded?: boolean;
  reduce: boolean;
  children: ReactNode;
  className?: string;
  [key: string]: unknown;
}) {
  return (
    <motion.button
      {...rest}
      type="button"
      aria-label={label}
      aria-expanded={expanded}
      onClick={onClick}
      disabled={disabled}
      whileTap={reduce || disabled ? undefined : { scale: 0.86 }}
      transition={SPRING_PRESS}
      className={cn(
        "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40",
        className,
      )}
    >
      {children}
    </motion.button>
  );
}
components/motion/availability-scheduler/time-select.tsx
"use client";

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/motion/select";
import type { TimeOption } from "./types";

// Time field: the library Select, with the option list capped so the panel
// measures a small height and scrolls instead of unfolding all 48 options.
export function TimeSelect({
  value,
  onChange,
  open,
  onOpenChange,
  options,
}: {
  value: string;
  onChange: (v: string) => void;
  open: boolean;
  onOpenChange: (open: boolean) => void;
  options: TimeOption[];
}) {
  return (
    <Select
      value={value}
      onValueChange={onChange}
      open={open}
      onOpenChange={onOpenChange}
      className="w-full"
    >
      <SelectTrigger className="tabular-nums">
        <SelectValue className="whitespace-nowrap" />
      </SelectTrigger>
      <SelectContent>
        <div className="max-h-56 overflow-y-auto overscroll-contain">
          {options.map((o) => (
            <SelectItem key={o.value} value={o.value} className="tabular-nums">
              {o.label}
            </SelectItem>
          ))}
        </div>
      </SelectContent>
    </Select>
  );
}
components/motion/switch.tsx
"use client";

import { animate, motion, MotionConfig, useReducedMotion } from "motion/react";
import { useEffect, useId, useRef, useState } from "react";
import { cn } from "@/lib/utils";

// Heavy, deliberate thumb — high mass keeps the travel weighty without wobble.
const THUMB_SPRING = { type: "spring", stiffness: 800, damping: 80, mass: 4 } as const;

export interface SwitchProps {
  checked: boolean;
  onCheckedChange: (checked: boolean) => void;
  disabled?: boolean;
  label?: string;
  ariaLabel?: string;
  className?: string;
}

export function Switch({
  checked,
  onCheckedChange,
  disabled,
  label,
  ariaLabel,
  className,
}: SwitchProps) {
  const id = useId();
  const thumbRef = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();
  const [isPressed, setIsPressed] = useState(false);
  const [isPointer, setIsPointer] = useState(false);

  // Disabled shake feedback when pressed.
  useEffect(() => {
    if (!thumbRef.current || reduce) return;
    if (disabled && isPressed) {
      animate(
        thumbRef.current,
        { x: [0, -2, 2, -1, 0] },
        { delay: 0.2, duration: 0.6 },
      );
    }
  }, [disabled, isPressed, reduce]);

  const squish = !disabled && isPointer && isPressed && !reduce;

  return (
    <MotionConfig transition={reduce ? { duration: 0 } : THUMB_SPRING}>
      <span className={cn("inline-flex items-center gap-3", className)}>
        <motion.button
          id={id}
          type="button"
          role="switch"
          aria-checked={checked}
          aria-label={ariaLabel}
          disabled={disabled}
          onClick={() => !disabled && onCheckedChange(!checked)}
          onPointerDown={(e) => {
            setIsPressed(true);
            setIsPointer(e.type.startsWith("pointer"));
          }}
          onPointerUp={() => setIsPressed(false)}
          onPointerLeave={() => setIsPressed(false)}
          initial={false}
          data-state={checked ? "checked" : "unchecked"}
          className={cn(
            "group peer inline-flex h-7 w-12 shrink-0 cursor-pointer items-center px-1 rounded-full outline-none transition-colors duration-200",
            "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
            "disabled:cursor-not-allowed disabled:opacity-60",
            checked ? "justify-end bg-primary" : "justify-start bg-muted-foreground/60",
          )}
        >
          <motion.div
            ref={thumbRef}
            layout
            animate={{ scale: squish ? 0.9 : 1 }}
            className="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-md"
          >
            {/* Stretch toward the destination while active. */}
            <div
              className={cn(
                "size-5",
                squish && (checked ? "ml-1" : "mr-1"),
              )}
            />
          </motion.div>
        </motion.button>
        {label ? (
          <label htmlFor={id} className="cursor-pointer text-sm text-foreground">
            {label}
          </label>
        ) : null}
      </span>
    </MotionConfig>
  );
}
components/motion/tooltip.tsx
"use client";

import { AnimatePresence } from "motion/react";
import {
  type ComponentProps,
  type PointerEvent as ReactPointerEvent,
  cloneElement,
  createContext,
  isValidElement,
  type ReactElement,
  type ReactNode,
  type Ref,
  type RefObject,
  useCallback,
  useContext,
  useEffect,
  useId,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { createPortal } from "react-dom";
import { TooltipSurface } from "@/components/motion/tooltip-surface";
import { useDismiss } from "@/lib/hooks/use-dismiss";
import { useHoverGesture } from "@/lib/hooks/use-hover-gesture";
import { useTapGesture } from "@/lib/hooks/use-tap-gesture";
import { cn } from "@/lib/utils";
import { focusFromKeyboard, graceArea, inBox, inPolygon, watchModality } from "./tooltip/grace";
import { TooltipArrow, TooltipPositioner } from "./tooltip/positioner";
import {
  type TooltipAlign,
  type TooltipAnchor,
  type TooltipPoint,
  type TooltipSide,
  useTooltipPointer,
} from "./tooltip/use-position";

export type { TooltipAlign, TooltipSide };

/* -------------------------------------------------------------------------- */
/* Provider                                                                   */
/* -------------------------------------------------------------------------- */

interface TooltipTiming {
  /** Delay before a hover opens a tooltip (ms). */
  delay: number;
  /** Delay before a tooltip closes once the pointer has left it (ms). */
  closeDelay: number;
  /** After one tooltip closes, others open instantly for this long (ms). */
  skipDelay: number;
}

const TimingContext = createContext<TooltipTiming>({ delay: 120, closeDelay: 0, skipDelay: 300 });

export interface TooltipProviderProps extends Partial<TooltipTiming> {
  children: ReactNode;
}

/** Shared timing for every tooltip inside it. Optional: tooltips work without one. */
export function TooltipProvider({ children, ...timing }: TooltipProviderProps) {
  const parent = useContext(TimingContext);
  const { delay = parent.delay, closeDelay = parent.closeDelay, skipDelay = parent.skipDelay } =
    timing;
  const value = useMemo(() => ({ delay, closeDelay, skipDelay }), [delay, closeDelay, skipDelay]);
  return <TimingContext.Provider value={value}>{children}</TimingContext.Provider>;
}

/* -------------------------------------------------------------------------- */
/* Root                                                                       */
/* -------------------------------------------------------------------------- */

// Moving along a toolbar should feel instant after the first label, and only
// one label may show at a time: a keyboard-focused control and a hovered one
// would otherwise both claim the screen.
let lastClosedAt = 0;
let current: { close: () => void } | null = null;

function isEmpty(content: ReactNode) {
  return content == null || content === false || content === "";
}

interface TooltipContextValue {
  open: boolean;
  contentId: string;
  labelId: string;
  asLabel: boolean;
  empty: boolean;
  followCursor: boolean;
  anchor: TooltipAnchor | null;
  /** True when a TooltipTrigger owns the anchor; false for an external `anchorRef`. */
  triggered: boolean;
  floatingRef: RefObject<HTMLSpanElement | null>;
  pointer: ReturnType<typeof useTooltipPointer>;
  setTrigger: (element: TooltipAnchor | null) => void;
  setEmpty: (empty: boolean) => void;
  close: () => void;
  onContentEnter: () => void;
  onContentLeave: (event: ReactPointerEvent) => void;
}

const TooltipContext = createContext<TooltipContextValue | null>(null);

function useTooltipContext(part: string) {
  const context = useContext(TooltipContext);
  if (!context) throw new Error(`${part} must be used inside <Tooltip>`);
  return context;
}

export interface TooltipRootProps {
  children?: ReactNode;
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  /** Delay before showing on hover (ms). Keyboard focus shows immediately. */
  delay?: number;
  /** Delay before hiding once the pointer has left (ms). */
  closeDelay?: number;
  /** Never show, e.g. while the label it carries is already visible. */
  disabled?: boolean;
  /**
   * Use the tooltip as the trigger's accessible name rather than its
   * description. For icon-only controls whose only label is the tooltip.
   */
  asLabel?: boolean;
  /** Follow real pointer coordinates; keyboard focus still uses the anchor. */
  followCursor?: boolean;
  /** Existing anchor for controlled integrations such as chart cells. */
  anchorRef?: RefObject<TooltipAnchor | null>;
  /** Id of the tooltip surface, for anchors that reference it themselves. */
  id?: string;
}

function TooltipRoot({
  children,
  open: controlledOpen,
  defaultOpen = false,
  onOpenChange,
  delay: delayProp,
  closeDelay: closeDelayProp,
  disabled = false,
  asLabel = false,
  followCursor = false,
  anchorRef,
  id,
  initialEmpty = false,
}: TooltipRootProps & { initialEmpty?: boolean }) {
  const timing = useContext(TimingContext);
  const delay = delayProp ?? timing.delay;
  const closeDelay = closeDelayProp ?? timing.closeDelay;
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
  const open = controlledOpen ?? uncontrolledOpen;
  const [empty, setEmpty] = useState(initialEmpty);
  const [trigger, setTrigger] = useState<TooltipAnchor | null>(null);
  const [external, setExternal] = useState<TooltipAnchor | null>(null);
  const anchor = anchorRef ? external : trigger;
  const generatedId = useId();
  const contentId = id ?? `${generatedId}-content`;
  const labelId = `${generatedId}-label`;
  const floatingRef = useRef<HTMLSpanElement | null>(null);
  const anchorObject = useRef<TooltipAnchor | null>(null);
  const pointer = useTooltipPointer(anchor, followCursor);

  // An external ref can attach after this component renders; read it once it has.
  useLayoutEffect(() => {
    if (!anchorRef) return;
    const element = anchorRef.current ?? null;
    if (element !== external) setExternal(element);
  });

  // Everything the event listeners read, current without re-binding them.
  const latest = useRef({ open, delay, closeDelay, disabled, empty, onOpenChange, controlled: false });
  useLayoutEffect(() => {
    anchorObject.current = anchor;
    latest.current = {
      open,
      delay,
      closeDelay,
      disabled,
      empty,
      onOpenChange,
      controlled: controlledOpen !== undefined,
    };
  });

  const openTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const grace = useRef<(() => void) | null>(null);
  const self = useRef<{ close: () => void }>({ close: () => {} });

  const clearTimers = useCallback(() => {
    if (openTimer.current) clearTimeout(openTimer.current);
    if (closeTimer.current) clearTimeout(closeTimer.current);
    openTimer.current = null;
    closeTimer.current = null;
  }, []);

  const stopGrace = useCallback(() => {
    grace.current?.();
    grace.current = null;
  }, []);

  // Only a real change reaches the consumer: a pointer leaving a closed
  // tooltip is not news.
  const commit = useCallback((next: boolean) => {
    if (latest.current.open === next) return;
    if (!latest.current.controlled) setUncontrolledOpen(next);
    latest.current.onOpenChange?.(next);
  }, []);

  const openNow = useCallback(() => {
    clearTimers();
    stopGrace();
    if (latest.current.disabled || latest.current.empty) return;
    if (current && current !== self.current) current.close();
    current = self.current;
    commit(true);
  }, [clearTimers, stopGrace, commit]);

  const close = useCallback(() => {
    clearTimers();
    stopGrace();
    if (latest.current.open) lastClosedAt = Date.now();
    if (current === self.current) current = null;
    commit(false);
  }, [clearTimers, stopGrace, commit]);
  useLayoutEffect(() => {
    self.current.close = close;
  }, [close]);

  const requestOpen = useCallback(() => {
    clearTimers();
    if (latest.current.open || latest.current.disabled || latest.current.empty) return;
    const warm = Date.now() - lastClosedAt < timing.skipDelay;
    if (warm || latest.current.delay <= 0) openNow();
    else openTimer.current = setTimeout(openNow, latest.current.delay);
  }, [clearTimers, openNow, timing.skipDelay]);

  const requestClose = useCallback(() => {
    clearTimers();
    if (latest.current.closeDelay > 0) {
      closeTimer.current = setTimeout(close, latest.current.closeDelay);
    } else close();
  }, [clearTimers, close]);

  // Hold the label open while the pointer crosses from one box toward the other.
  const startGrace = useCallback(
    (exit: TooltipPoint, from: Element, to: Element | null) => {
      stopGrace();
      if (!to) return requestClose();
      const target = to.getBoundingClientRect();
      const origin = from.getBoundingClientRect();
      const corridor = graceArea(exit, target);
      const onMove = (event: PointerEvent) => {
        const point = { x: event.clientX, y: event.clientY };
        if (inBox(point, target) || inBox(point, origin)) return;
        if (!inPolygon(point, corridor)) requestClose();
      };
      document.addEventListener("pointermove", onMove);
      grace.current = () => document.removeEventListener("pointermove", onMove);
    },
    [stopGrace, requestClose],
  );

  const hover = useHoverGesture();
  const tap = useTapGesture<boolean>();

  // Listeners go on the trigger itself rather than on cloned props, so a
  // component that owns its own handlers keeps them, and no wrapper element
  // sits between the control and its layout.
  useEffect(() => {
    if (anchorRef || !trigger) return;
    watchModality();
    // A mouse press dismisses the label and keeps it away until the pointer
    // leaves; hovering a control you just clicked should not bring it back.
    let suppressed = false;
    // Focus that came from the keyboard holds the label open on its own.
    let keyboardFocus = false;

    const onEnter = (event: PointerEvent) => {
      if (!hover.enter(event)) return;
      stopGrace();
      if (closeTimer.current) clearTimeout(closeTimer.current);
      if (!suppressed) requestOpen();
    };
    const onLeave = (event: PointerEvent) => {
      if (!hover.leave(event)) return;
      suppressed = false;
      if (keyboardFocus) return;
      if (openTimer.current) {
        clearTimers();
        return;
      }
      if (latest.current.open && !followCursor) {
        startGrace({ x: event.clientX, y: event.clientY }, trigger, floatingRef.current);
      } else requestClose();
    };
    const onDown = (event: PointerEvent) => {
      tap.start(event, latest.current.open);
      if (event.pointerType === "mouse") {
        suppressed = true;
        close();
      }
    };
    // A finger never hovers, and Safari does not focus a button on tap either,
    // so the label is only reachable if the tap itself toggles it. The click
    // carries no pointerType; the pointerdown before it says whether it was a
    // tap, and keyboard activation has none at all.
    const onClick = () => {
      const gesture = tap.take();
      if (!gesture || gesture.pointerType === "mouse") return;
      if (gesture.state) close();
      else openNow();
    };
    const onKey = () => tap.drop();
    const onFocusIn = () => {
      if (!focusFromKeyboard()) return;
      keyboardFocus = true;
      openNow();
    };
    const onFocusOut = (event: FocusEvent) => {
      if (trigger.contains(event.relatedTarget as Node | null)) return;
      keyboardFocus = false;
      close();
    };

    // Both element kinds dispatch the same events; one type keeps the listeners typed.
    const el = trigger as HTMLElement;
    el.addEventListener("pointerenter", onEnter);
    el.addEventListener("pointerleave", onLeave);
    el.addEventListener("pointerdown", onDown);
    el.addEventListener("pointercancel", tap.drop);
    el.addEventListener("click", onClick);
    el.addEventListener("keydown", onKey);
    el.addEventListener("focusin", onFocusIn);
    el.addEventListener("focusout", onFocusOut);
    return () => {
      el.removeEventListener("pointerenter", onEnter);
      el.removeEventListener("pointerleave", onLeave);
      el.removeEventListener("pointerdown", onDown);
      el.removeEventListener("pointercancel", tap.drop);
      el.removeEventListener("click", onClick);
      el.removeEventListener("keydown", onKey);
      el.removeEventListener("focusin", onFocusIn);
      el.removeEventListener("focusout", onFocusOut);
    };
  }, [
    anchorRef,
    trigger,
    followCursor,
    hover,
    tap,
    requestOpen,
    requestClose,
    openNow,
    close,
    clearTimers,
    stopGrace,
    startGrace,
  ]);

  // While open, a trigger that turns disabled loses focus without a blur event,
  // and a window that loses focus never reports the pointer leaving.
  useEffect(() => {
    if (!open || anchorRef || !trigger) return;
    const observer = new MutationObserver(() => {
      if (trigger.matches(":disabled, [aria-disabled='true']")) close();
    });
    observer.observe(trigger, { attributes: true, attributeFilter: ["disabled", "aria-disabled"] });
    window.addEventListener("blur", close);
    return () => {
      observer.disconnect();
      window.removeEventListener("blur", close);
    };
  }, [open, anchorRef, trigger, close]);

  // A chart drives its own open state and swaps content as it goes; only a
  // trigger-owned label closes itself for having nothing to say.
  useEffect(() => {
    if (disabled || (empty && !anchorRef)) close();
  }, [disabled, empty, anchorRef, close]);

  useEffect(
    () => () => {
      clearTimers();
      stopGrace();
      if (current === self.current) current = null;
    },
    [clearTimers, stopGrace],
  );

  const insideTooltip = useCallback(
    (target: Element) => Boolean(floatingRef.current?.contains(target)),
    [],
  );
  // ...and closed by the next tap that lands somewhere else, or Escape. The
  // label covers nothing interactive, so that tap passes through to what it hit.
  useDismiss(open, close, anchorObject, { ignore: insideTooltip });

  const onContentEnter = useCallback(() => {
    stopGrace();
    clearTimers();
  }, [stopGrace, clearTimers]);

  const onContentLeave = useCallback(
    (event: ReactPointerEvent) => {
      const floating = floatingRef.current;
      if (!floating) return;
      if (anchorRef) {
        // Bridge the small gap back onto a chart; it reopens on its own terms.
        clearTimers();
        closeTimer.current = setTimeout(close, 100);
        return;
      }
      startGrace({ x: event.clientX, y: event.clientY }, floating, anchorObject.current);
    },
    [anchorRef, clearTimers, close, startGrace],
  );

  const value = useMemo<TooltipContextValue>(
    () => ({
      open,
      contentId,
      labelId,
      asLabel,
      empty,
      followCursor,
      anchor,
      triggered: !anchorRef,
      floatingRef,
      pointer,
      setTrigger,
      setEmpty,
      close,
      onContentEnter,
      onContentLeave,
    }),
    [
      open,
      contentId,
      labelId,
      asLabel,
      empty,
      followCursor,
      anchor,
      anchorRef,
      pointer,
      close,
      onContentEnter,
      onContentLeave,
    ],
  );

  return <TooltipContext.Provider value={value}>{children}</TooltipContext.Provider>;
}

/* -------------------------------------------------------------------------- */
/* Trigger                                                                    */
/* -------------------------------------------------------------------------- */

function mergeIds(...ids: unknown[]) {
  return ids.filter((value) => typeof value === "string" && value).join(" ") || undefined;
}

function assignRef<T>(ref: Ref<T> | undefined, value: T | null) {
  if (typeof ref === "function") ref(value);
  else if (ref) (ref as { current: T | null }).current = value;
}

export interface TooltipTriggerProps extends ComponentProps<"button"> {
  /** Use the single child element as the trigger instead of rendering a button. */
  asChild?: boolean;
}

export function TooltipTrigger({ asChild = false, children, ref, ...props }: TooltipTriggerProps) {
  const { open, contentId, labelId, asLabel, empty, setTrigger } =
    useTooltipContext("TooltipTrigger");
  const child = asChild && isValidElement(children) ? children : null;
  const childProps = (child?.props ?? props) as Record<string, unknown>;
  const childRef = child ? (childProps.ref as Ref<TooltipAnchor> | undefined) : undefined;
  const attached = useRef<TooltipAnchor | null>(null);

  const setRef = useCallback(
    (element: TooltipAnchor | null) => {
      attached.current = element;
      setTrigger(element);
      assignRef(childRef ?? (ref as Ref<TooltipAnchor> | undefined), element);
    },
    [setTrigger, childRef, ref],
  );

  useEffect(() => {
    if (process.env.NODE_ENV !== "production" && child && !attached.current) {
      console.warn(
        "TooltipTrigger: the child did not attach a ref, so the tooltip has nothing to listen to. Render a DOM element or a component that passes `ref` through.",
      );
    }
  }, [child]);

  const aria = asLabel
    ? {
        "aria-labelledby": empty
          ? (childProps["aria-labelledby"] as string | undefined)
          : labelId,
      }
    : {
        "aria-describedby": mergeIds(
          childProps["aria-describedby"],
          open && !empty ? contentId : undefined,
        ),
      };

  if (asChild) {
    if (!child) return children;
    return cloneElement(child as ReactElement<Record<string, unknown>>, { ref: setRef, ...aria });
  }

  return (
    <button
      type="button"
      data-state={open ? "open" : "closed"}
      {...props}
      ref={setRef as Ref<HTMLButtonElement>}
      {...aria}
    >
      {children}
    </button>
  );
}

/* -------------------------------------------------------------------------- */
/* Content                                                                    */
/* -------------------------------------------------------------------------- */

export interface TooltipContentProps {
  children?: ReactNode;
  side?: TooltipSide;
  align?: TooltipAlign;
  /** Gap between anchor and tooltip (px). */
  sideOffset?: number;
  /** Draw an arrow pointing at the anchor. */
  arrow?: boolean;
  /** Point within the anchor, as fractions of its rendered width and height. */
  anchorPoint?: TooltipPoint;
  className?: string;
}

export function TooltipContent({
  children,
  side = "top",
  align = "center",
  sideOffset,
  arrow = false,
  anchorPoint,
  className,
}: TooltipContentProps) {
  const {
    open,
    contentId,
    labelId,
    asLabel,
    followCursor,
    anchor,
    floatingRef,
    pointer,
    setEmpty,
    close,
    onContentEnter,
    onContentLeave,
  } = useTooltipContext("TooltipContent");
  const empty = isEmpty(children);
  const arrowRef = useRef<SVGSVGElement | null>(null);

  useLayoutEffect(() => {
    setEmpty(empty);
  }, [empty, setEmpty]);

  if (typeof document === "undefined") return null;

  return createPortal(
    <>
      {/* The name has to exist while the label is closed, so it lives apart from the surface. */}
      {asLabel && !empty ? (
        <span id={labelId} hidden>
          {children}
        </span>
      ) : null}
      <AnimatePresence>
        {open && !empty ? (
          <TooltipPositioner
            key="tooltip"
            anchor={anchor}
            floatingRef={floatingRef}
            arrowRef={arrow ? arrowRef : undefined}
            anchorPoint={anchorPoint}
            followCursor={followCursor}
            side={side}
            align={align}
            sideOffset={sideOffset}
            onDismiss={close}
            pointer={pointer}
          >
            {(positioned, isPresent, placedSide) => (
              <TooltipSurface
                id={asLabel ? undefined : contentId}
                aria-hidden={asLabel || undefined}
                ready={positioned}
                onPointerEnter={onContentEnter}
                onPointerLeave={onContentLeave}
                style={{ pointerEvents: isPresent && !followCursor ? "auto" : "none" }}
                className={cn(
                  "max-w-[min(20rem,calc(100vw-16px))] whitespace-normal break-words",
                  arrow && "relative",
                  className,
                )}
              >
                {children}
                {arrow ? <TooltipArrow ref={arrowRef} side={placedSide} /> : null}
              </TooltipSurface>
            )}
          </TooltipPositioner>
        ) : null}
      </AnimatePresence>
    </>,
    document.body,
  );
}

/* -------------------------------------------------------------------------- */
/* Tooltip (root, or one-line shorthand when given `content`)                 */
/* -------------------------------------------------------------------------- */

export interface TooltipProps
  extends TooltipRootProps,
    Omit<TooltipContentProps, "children" | "className"> {
  /**
   * Shorthand: the tooltip's content. With it, `children` is the trigger
   * element; without it, compose `TooltipTrigger` and `TooltipContent`.
   */
  content?: ReactNode;
  /** Shorthand only: classes for the tooltip surface. */
  className?: string;
}

export function Tooltip(props: TooltipProps) {
  if (!("content" in props)) return <TooltipRoot {...props} />;
  const {
    content,
    children,
    side,
    align,
    sideOffset,
    arrow,
    anchorPoint,
    className,
    ...root
  } = props;
  if (!root.anchorRef && !isValidElement(children)) return children ?? null;
  return (
    <TooltipRoot {...root} initialEmpty={isEmpty(content)}>
      {isValidElement(children) ? <TooltipTrigger asChild>{children}</TooltipTrigger> : null}
      <TooltipContent
        side={side}
        align={align}
        sideOffset={sideOffset}
        arrow={arrow}
        anchorPoint={anchorPoint}
        className={className}
      >
        {content}
      </TooltipContent>
    </TooltipRoot>
  );
}
lib/ease.ts
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.

export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;

/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";

/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
  type: "spring",
  stiffness: 500,
  damping: 30,
  mass: 0.6,
} as const;

/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
  type: "spring",
  stiffness: 460,
  damping: 30,
  mass: 0.55,
} as const;

/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
  type: "spring",
  stiffness: 420,
  damping: 40,
  mass: 0.5,
} as const;

/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
  type: "spring",
  stiffness: 360,
  damping: 32,
  mass: 0.6,
} as const;

/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
  stiffness: 200,
  damping: 15,
  mass: 0.3,
} as const;

/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
 * so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
  stiffness: 700,
  damping: 50,
  mass: 0.5,
} as const;
components/motion/checkbox.tsx
"use client";

import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useId } from "react";
import { EASE_OUT, SPRING_PRESS } from "@/lib/ease";
import { cn } from "@/lib/utils";

const CHECK_PATH = "M5 13l4 4L19 7";
const INDETERMINATE_PATH = "M6 12h12";

export interface CheckboxProps {
  checked: boolean;
  onCheckedChange: (checked: boolean) => void;
  disabled?: boolean;
  indeterminate?: boolean;
  label?: string;
  /** Supporting text under the label, announced as the description rather than the name. */
  description?: string;
  className?: string;
  id?: string;
  "aria-label"?: string;
  /** Associates an external message (e.g. a form error) with the control. */
  "aria-describedby"?: string;
}

export function Checkbox({
  checked,
  onCheckedChange,
  disabled,
  indeterminate,
  label,
  description,
  className,
  id: idProp,
  "aria-label": ariaLabel,
  "aria-describedby": ariaDescribedBy,
}: CheckboxProps) {
  const autoId = useId();
  const id = idProp ?? autoId;
  const reduce = useReducedMotion();
  const showMark = checked || indeterminate;
  const path = indeterminate ? INDETERMINATE_PATH : CHECK_PATH;
  const descriptionId = description ? `${id}-description` : undefined;
  // The wrapping <label> would fold the description into the name, so name from the label text alone.
  const labelId = description && label && !ariaLabel ? `${id}-label` : undefined;
  const describedBy =
    [ariaDescribedBy, descriptionId].filter(Boolean).join(" ") || undefined;

  return (
    <label
      htmlFor={id}
      className={cn(
        "inline-flex gap-3",
        description ? "items-start" : "items-center",
        disabled ? "cursor-not-allowed" : "cursor-pointer",
        className,
      )}
    >
      <motion.button
        id={id}
        type="button"
        role="checkbox"
        aria-checked={indeterminate ? "mixed" : checked}
        aria-label={ariaLabel}
        aria-labelledby={labelId}
        aria-describedby={describedBy}
        disabled={disabled}
        onClick={() => !disabled && onCheckedChange(!checked)}
        whileTap={reduce || disabled ? undefined : { scale: 0.92 }}
        transition={SPRING_PRESS}
        data-state={
          checked ? "checked" : indeterminate ? "indeterminate" : "unchecked"
        }
        className={cn(
          "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border-2 outline-none transition-colors duration-200",
          "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
          "disabled:cursor-not-allowed disabled:opacity-60",
          showMark
            ? "border-primary bg-primary text-primary-foreground"
            : "border-muted-foreground/50 bg-background hover:border-muted-foreground",
        )}
      >
        <AnimatePresence initial={false}>
          {showMark ? (
            <motion.svg
              key={indeterminate ? "indeterminate" : "checked"}
              width="12"
              height="12"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth={3}
              strokeLinecap="round"
              strokeLinejoin="round"
              initial={reduce ? { opacity: 1 } : { opacity: 0, scale: 0.5 }}
              animate={reduce ? { opacity: 1 } : { opacity: 1, scale: 1 }}
              exit={
                reduce
                  ? { opacity: 0 }
                  : { opacity: 0, scale: 0.5, filter: "blur(4px)" }
              }
              transition={
                reduce ? { duration: 0 } : { duration: 0.16, ease: EASE_OUT }
              }
              aria-hidden
            >
              <title>{indeterminate ? "Partially selected" : "Selected"}</title>
              <motion.path
                d={path}
                initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}
                animate={{ pathLength: 1 }}
                transition={
                  reduce
                    ? { duration: 0 }
                    : {
                        duration: indeterminate ? 0.2 : 0.3,
                        ease: EASE_OUT,
                        delay: 0.04,
                      }
                }
              />
            </motion.svg>
          ) : null}
        </AnimatePresence>
      </motion.button>
      {label || description ? (
        <span className={cn("flex min-w-0 flex-col gap-0.5 select-none", disabled && "opacity-60")}>
          {label ? (
            <span
              id={labelId}
              // Geist sits low in a 20px line; a 16px line lifts the text onto the control.
              className={cn("text-sm text-foreground", description && "leading-4")}
            >
              {label}
            </span>
          ) : null}
          {description ? (
            <span id={descriptionId} className="text-xs text-muted-foreground">
              {description}
            </span>
          ) : null}
        </span>
      ) : null}
    </label>
  );
}
components/motion/popover-morph.tsx
"use client";

import {
  AnimatePresence,
  animate,
  motion,
  useMotionValue,
  usePresence,
  useReducedMotion,
  useTransform,
} from "motion/react";
import {
  cloneElement,
  createContext,
  isValidElement,
  type ReactElement,
  type ReactNode,
  type Ref,
  useCallback,
  useContext,
  useEffect,
  useId,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { createPortal } from "react-dom";
import { usePopoverPortalPosition } from "@/components/motion/popover-position";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";

type Side = "top" | "bottom";
type Align = "start" | "end";

type MorphContextValue = {
  open: boolean;
  setOpen: (open: boolean) => void;
  toggle: () => void;
  triggerId: string;
  contentId: string;
  /** The element the panel measures against — see `registerTrigger`. */
  triggerRef: React.MutableRefObject<HTMLElement | null>;
  registerTrigger: (node: HTMLElement | null) => void;
  contentRef: React.MutableRefObject<HTMLDivElement | null>;
};

const MorphContext = createContext<MorphContextValue | null>(null);

function useMorphContext(component: string) {
  const ctx = useContext(MorphContext);
  if (!ctx) throw new Error(`${component} must be used within <MorphPopover>`);
  return ctx;
}

export interface MorphPopoverProps {
  children: ReactNode;
  /** Controlled open state. */
  open?: boolean;
  /** Uncontrolled initial open state. */
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  className?: string;
}

/**
 * A popover whose full-size panel reveals from the corner nearest the trigger
 * on one spring, then retraces the same clip on close. Content and surface
 * edges keep their size throughout. Closes on outside pointer / Escape.
 * Controlled or uncontrolled.
 */
export function MorphPopover({
  children,
  open: controlledOpen,
  defaultOpen = false,
  onOpenChange,
  className,
}: MorphPopoverProps) {
  const baseId = useId();
  const [root, setRoot] = useState<HTMLDivElement | null>(null);
  const [trigger, setTrigger] = useState<HTMLElement | null>(null);
  const contentRef = useRef<HTMLDivElement | null>(null);
  const [internalOpen, setInternalOpen] = useState(defaultOpen);
  const controlled = controlledOpen !== undefined;
  const open = controlled ? controlledOpen : internalOpen;

  const setOpen = useCallback(
    (next: boolean) => {
      if (!controlled) setInternalOpen(next);
      onOpenChange?.(next);
    },
    [controlled, onOpenChange],
  );
  const toggle = useCallback(() => setOpen(!open), [setOpen, open]);

  // A trigger normally registers itself through MorphPopoverTrigger. It can't
  // when something else already clones the element — a Tooltip wrapping the
  // button, say — and an unregistered trigger leaves the panel with nothing to
  // measure against, so it renders permanently invisible. The root boxes the
  // trigger exactly (the content portals out of it), so it stands in until a
  // real trigger registers, and stands in again if that one unmounts. Both are
  // state, so a trigger arriving while the panel is open re-anchors it.
  const anchorRef = useMemo<React.MutableRefObject<HTMLElement | null>>(
    () => ({ current: trigger ?? root }),
    [root, trigger],
  );

  // The panel is a `role="dialog"` and goes inert the moment it closes, so
  // focus cannot be left sitting inside it: a dismissal hands it back to the
  // trigger, the way the ARIA dialog pattern asks. A pointer dismissal takes
  // the focus onward itself when it lands on something focusable — this only
  // catches the case where it would otherwise be stranded. When no trigger has
  // registered, the root anchor stands in only if it can actually hold focus;
  // there is nowhere better than where the keyboard already is, so leave it.
  const close = useCallback(() => {
    setOpen(false);
    const focused = document.activeElement;
    const inPanel =
      focused instanceof HTMLElement && contentRef.current?.contains(focused);
    if (!inPanel) return;
    const restore = trigger ?? (root && root.tabIndex >= 0 ? root : null);
    restore?.focus();
  }, [root, setOpen, trigger]);

  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => e.key === "Escape" && close();
    const onPointer = (e: PointerEvent) => {
      const target = e.target as Node;
      if (
        root &&
        !root.contains(target) &&
        !contentRef.current?.contains(target)
      )
        close();
    };
    window.addEventListener("keydown", onKey);
    window.addEventListener("pointerdown", onPointer);
    return () => {
      window.removeEventListener("keydown", onKey);
      window.removeEventListener("pointerdown", onPointer);
    };
  }, [open, root, close]);

  const ctx = useMemo<MorphContextValue>(
    () => ({
      open,
      setOpen,
      toggle,
      triggerId: `${baseId}-trigger`,
      contentId: `${baseId}-content`,
      triggerRef: anchorRef,
      registerTrigger: setTrigger,
      contentRef,
    }),
    [open, setOpen, toggle, baseId, anchorRef],
  );

  return (
    <MorphContext.Provider value={ctx}>
      <div ref={setRoot} className={cn("relative inline-flex", className)}>
        {children}
      </div>
    </MorphContext.Provider>
  );
}

export interface MorphPopoverTriggerProps {
  children: ReactElement;
}

function mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (typeof ref === "function") ref(node);
      else if (ref && typeof ref === "object")
        (ref as React.MutableRefObject<T | null>).current = node;
    }
  };
}

/** Wraps a single element, toggling the popover on click. */
export function MorphPopoverTrigger({ children }: MorphPopoverTriggerProps) {
  const ctx = useMorphContext("MorphPopoverTrigger");
  const child = children as ReactElement<Record<string, unknown>>;
  const childOnClick = child?.props?.onClick as
    | ((e: unknown) => void)
    | undefined;
  const childRef = (child?.props as { ref?: Ref<HTMLElement> } | undefined)
    ?.ref;
  // Register once per actual ref change, not once per open-state render.
  const mergedRef = useMemo(
    () => mergeRefs(childRef, ctx.registerTrigger),
    [childRef, ctx.registerTrigger],
  );
  if (!isValidElement(children)) return children;

  return cloneElement(child, {
    id: ctx.triggerId,
    ref: mergedRef,
    onClick: (e: unknown) => {
      childOnClick?.(e);
      ctx.toggle();
    },
    "aria-haspopup": "dialog",
    "aria-expanded": ctx.open,
    "aria-controls": ctx.open ? ctx.contentId : undefined,
  });
}

// Full-distance clip travel needs a shorter curve than SPRING_PANEL's slow
// overdamped tail. A non-bouncing spring reaches its rest within 260 ms.
const MORPH_ENTER = { type: "spring", duration: 0.26, bounce: 0 } as const;
const MORPH_EXIT = { duration: 0.2, ease: EASE_OUT } as const;

// Keep the panel and content at their final size. An inset clip reveals the
// nearest corner without scaling borders, shadows or keyboard-focus geometry.
// A full-size clip keeps content at its final coordinates without box-to-box
// scale correction. One progress value keeps clip and opacity synchronized;
// clip-path and the shadow may paint, so this is not a compositor-only morph.
function clipAt(side: Side, align: Align, radius: number, inset: number) {
  return `inset(${side === "top" ? inset : 0}% ${align === "start" ? inset : 0}% ${side === "bottom" ? inset : 0}% ${align === "end" ? inset : 0}% round ${radius}px)`;
}

export interface MorphPopoverContentProps {
  children: ReactNode;
  side?: Side;
  align?: Align;
  /** Gap between trigger and panel, in px. Default 8. */
  sideOffset?: number;
  /** Panel corner radius, in px. Default 16. */
  radius?: number;
  /** Draw the surface shadow. Default true. */
  shadow?: boolean;
  /** Runs after the portalled surface is positioned and visible. */
  onOpenAutoFocus?: (content: HTMLDivElement) => void;
  className?: string;
}

export function MorphPopoverContent(props: MorphPopoverContentProps) {
  const ctx = useMorphContext("MorphPopoverContent");
  const [portalReady, setPortalReady] = useState(false);
  useEffect(() => setPortalReady(true), []);
  if (!portalReady) return null;
  return createPortal(
    <AnimatePresence>
      {ctx.open && <MorphPopoverSurface {...props} />}
    </AnimatePresence>,
    document.body,
  );
}

// Measurement belongs to the mounted portal session: reopening must not start
// an entrance at the previous session's coordinates before measuring this one.
function MorphPopoverSurface({
  children,
  side = "bottom",
  align = "end",
  sideOffset = 8,
  radius = 16,
  shadow = true,
  onOpenAutoFocus,
  className,
}: MorphPopoverContentProps) {
  const ctx = useMorphContext("MorphPopoverContent");
  const reduce = useReducedMotion() ?? false;
  const [isPresent, safeToRemove] = usePresence();
  const layout = usePopoverPortalPosition(
    ctx.triggerRef,
    ctx.contentRef,
    isPresent,
  );

  const preferredLeft = layout
    ? align === "end"
      ? layout.trigger.left + layout.trigger.width - layout.content.width
      : layout.trigger.left
    : 0;
  // Keep larger compositions, such as calendars, inside the viewport. Prefer
  // the requested side, then use the roomier side when it cannot fit there.
  const gutter = 12;
  const below = layout
    ? Math.max(
        0,
        window.innerHeight -
          layout.trigger.top -
          layout.trigger.height -
          sideOffset -
          gutter,
      )
    : 0;
  const above = layout
    ? Math.max(0, layout.trigger.top - sideOffset - gutter)
    : 0;
  const requestedSpace = side === "bottom" ? below : above;
  const otherSpace = side === "bottom" ? above : below;
  const resolvedSide =
    layout &&
    layout.content.height > requestedSpace &&
    otherSpace > requestedSpace
      ? side === "bottom"
        ? "top"
        : "bottom"
      : side;
  const availableHeight = resolvedSide === "bottom" ? below : above;
  const left = layout
    ? Math.max(
        gutter,
        Math.min(
          preferredLeft,
          window.innerWidth - layout.content.width - gutter,
        ),
      )
    : 0;
  const top = layout
    ? resolvedSide === "bottom"
      ? layout.trigger.top + layout.trigger.height + sideOffset
      : Math.max(
          gutter,
          layout.trigger.top - layout.content.height - sideOffset,
        )
    : 0;

  const progress = useMotionValue(0);
  const clipPath = useTransform(progress, (p) =>
    clipAt(resolvedSide, align, radius, 92 * (1 - p)),
  );
  const surfaceOpacity = useTransform(progress, [0, 0.15], [0, 1]);
  // Reduced motion has no travel, so a short fade carries the whole entrance
  // and exit on its own.
  const fade = useMotionValue(reduce ? 0 : 1);
  const ready = layout !== null;
  const focusedOnOpen = useRef(false);
  useEffect(() => {
    if (
      !ready ||
      !isPresent ||
      focusedOnOpen.current ||
      !ctx.contentRef.current
    )
      return;
    focusedOnOpen.current = true;
    onOpenAutoFocus?.(ctx.contentRef.current);
  }, [ready, isPresent, ctx.contentRef, onOpenAutoFocus]);
  // `safeToRemove` is a new function on every render: AnimatePresence
  // rebuilds its context each time its parent renders. Read it through a ref
  // so a render during the exit (a consumer's state change, a list streaming
  // in) does not restart the exit from where it got to — restarted every
  // render, it would close in ever smaller steps and never report completion,
  // leaving an inert, invisible panel mounted for good.
  const safeToRemoveRef = useRef(safeToRemove);
  useLayoutEffect(() => {
    safeToRemoveRef.current = safeToRemove;
  });
  useEffect(() => {
    if (!ready) {
      if (!isPresent) safeToRemoveRef.current?.();
      return;
    }
    const settle = () => {
      if (!isPresent) safeToRemoveRef.current?.();
    };
    if (reduce) {
      progress.jump(1);
      const animation = animate(fade, isPresent ? 1 : 0, {
        duration: 0.12,
        onComplete: settle,
      });
      return () => animation.stop();
    }
    const animation = animate(progress, isPresent ? 1 : 0, {
      ...(isPresent ? MORPH_ENTER : MORPH_EXIT),
      onComplete: settle,
    });
    return () => animation.stop();
  }, [fade, progress, ready, isPresent, reduce]);

  return (
    <motion.div
      data-morph-popover-portal=""
      inert={!isPresent}
      style={{
        left,
        top,
        opacity: reduce ? fade : surfaceOpacity,
        pointerEvents: isPresent ? "auto" : "none",
        visibility: layout ? "visible" : "hidden",
      }}
      // A drop-shadow follows the clipped surface; box-shadow would be cut
      // off by its own clip. Caller surface classes retain their old target.
      className={cn(
        "fixed z-[9999]",
        shadow && "[filter:drop-shadow(0_10px_18px_rgba(0,0,0,0.14))]",
      )}
    >
      <motion.div
        ref={ctx.contentRef}
        id={ctx.contentId}
        role="dialog"
        aria-labelledby={ctx.triggerId}
        style={{
          clipPath: reduce ? undefined : clipPath,
          borderRadius: radius,
          maxHeight: layout ? availableHeight : undefined,
          overflowX: "hidden",
          overflowY: "auto",
        }}
        className={cn(
          "overflow-hidden border border-border bg-background",
          className,
        )}
      >
        {children}
      </motion.div>
    </motion.div>
  );
}
components/motion/select.tsx
"use client";

import { Check, ChevronDown } from "lucide-react";
import {
  animate,
  type MotionValue,
  motion,
  useMotionValue,
  useMotionValueEvent,
  useReducedMotion,
  useTransform,
  type Variants,
} from "motion/react";
import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useEffect,
  useId,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";

// One progress value drives the surface, gap, corners and chevron. This short
// spring suits the full reveal distance; each reader clamps its small internal
// overshoot, so all visible endpoints settle together. Rows share a short
// reveal so long lists have no late entries.
const UNFOLD_SPRING = {
  type: "spring",
  visualDuration: 0.2,
  bounce: 0.12,
} as const;
// Exits are faster than entrances: the panel folds back on a short ease.
const UNFOLD_EXIT = { duration: 0.2, ease: EASE_OUT } as const;
const RADIUS = 12;
// How far the open panel sits from the trigger once it has pulled away.
const GAP = 8;
// The surface starts as a thin lip flush against the trigger's edge.
const LIP = 8;
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;

const ITEM_VARIANTS: Variants = {
  // A shared short reveal keeps late rows in long, scrolled lists ready with
  // the surface. Per-index staggering can delay visible rows by over a second.
  hidden: { opacity: 0, y: -4, transition: { duration: 0.08, ease: EASE_OUT } },
  show: { opacity: 1, y: 0, transition: { duration: 0.12, ease: EASE_OUT } },
};

type Placement = "bottom" | "top";

interface SelectContextValue {
  value: string | undefined;
  open: boolean;
  setOpen: (open: boolean) => void;
  select: (value: string) => void;
  register: (value: string, label: string) => void;
  unregister: (value: string) => void;
  labelFor: (value: string | undefined) => string | undefined;
  reduce: boolean;
  progress: MotionValue<number>;
  triggerId: string;
  listId: string;
  disabled: boolean;
  placement: Placement;
  setPlacement: (p: Placement) => void;
}

const SelectContext = createContext<SelectContextValue | null>(null);

function useSelectContext(component: string) {
  const ctx = useContext(SelectContext);
  if (!ctx) throw new Error(`${component} must be used within <Select>`);
  return ctx;
}

export interface SelectProps {
  value?: string;
  defaultValue?: string;
  onValueChange?: (value: string) => void;
  /**
   * Controlled open state of the panel. A layout that stacks selects can hold
   * this to keep exactly one panel open — the panel is absolutely positioned
   * inside its field, so two open at once paint over each other's options.
   */
  open?: boolean;
  /** Uncontrolled initial open state. Default false. */
  defaultOpen?: boolean;
  /**
   * Fires whenever the panel opens or closes. The panel is absolutely
   * positioned inside the field, so a layout that stacks selects has to know
   * which one is open to paint it above its neighbours.
   */
  onOpenChange?: (open: boolean) => void;
  disabled?: boolean;
  className?: string;
  children: ReactNode;
}

export function Select({
  value,
  defaultValue,
  onValueChange,
  open: openProp,
  defaultOpen = false,
  onOpenChange,
  disabled = false,
  className,
  children,
}: SelectProps) {
  const reduce = useReducedMotion() ?? false;
  const baseId = useId();
  const rootRef = useRef<HTMLDivElement>(null);
  const [internalOpen, setInternalOpen] = useState(defaultOpen);
  const [internal, setInternal] = useState(defaultValue);
  const [labels, setLabels] = useState<Map<string, string>>(new Map());
  const [placement, setPlacement] = useState<Placement>("bottom");

  const controlled = value !== undefined;
  const current = controlled ? value : internal;
  const openControlled = openProp !== undefined;
  const open = openControlled ? openProp : internalOpen;
  const progress = useMotionValue(open ? 1 : 0);

  const setOpen = useCallback(
    (next: boolean) => {
      if (!openControlled) setInternalOpen(next);
      onOpenChange?.(next);
    },
    [onOpenChange, openControlled],
  );

  const select = useCallback(
    (next: string) => {
      if (!controlled) setInternal(next);
      onValueChange?.(next);
      setOpen(false);
    },
    [controlled, onValueChange, setOpen],
  );

  const register = useCallback((v: string, label: string) => {
    setLabels((m) => (m.get(v) === label ? m : new Map(m).set(v, label)));
  }, []);
  const unregister = useCallback((v: string) => {
    setLabels((m) => {
      if (!m.has(v)) return m;
      const next = new Map(m);
      next.delete(v);
      return next;
    });
  }, []);

  // Reduced motion has no travel, so the progress only carries a short fade.
  useEffect(() => {
    const animation = animate(
      progress,
      open ? 1 : 0,
      reduce ? { duration: 0.12 } : open ? UNFOLD_SPRING : UNFOLD_EXIT,
    );
    return () => animation.stop();
  }, [open, progress, reduce]);

  // close on outside pointer / escape
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
    const onPointer = (e: PointerEvent) => {
      if (rootRef.current && !rootRef.current.contains(e.target as Node))
        setOpen(false);
    };
    window.addEventListener("keydown", onKey);
    window.addEventListener("pointerdown", onPointer);
    return () => {
      window.removeEventListener("keydown", onKey);
      window.removeEventListener("pointerdown", onPointer);
    };
  }, [open, setOpen]);

  const ctx = useMemo<SelectContextValue>(
    () => ({
      value: current,
      open,
      setOpen,
      select,
      register,
      unregister,
      labelFor: (v) => (v === undefined ? undefined : labels.get(v)),
      reduce,
      progress,
      triggerId: `${baseId}-trigger`,
      listId: `${baseId}-list`,
      disabled,
      placement,
      setPlacement,
    }),
    [
      current,
      open,
      setOpen,
      select,
      register,
      unregister,
      labels,
      reduce,
      progress,
      baseId,
      disabled,
      placement,
    ],
  );

  return (
    <SelectContext.Provider value={ctx}>
      <div ref={rootRef} className={cn("relative", className)}>
        {children}
      </div>
    </SelectContext.Provider>
  );
}

export interface SelectTriggerProps {
  className?: string;
  children: ReactNode;
}

export function SelectTrigger({ className, children }: SelectTriggerProps) {
  const ctx = useSelectContext("SelectTrigger");
  const isTop = ctx.placement === "top";
  // The edge facing the panel flattens while the panel is attached to it and
  // rounds back as the panel pulls away — the two pinch apart — on the same
  // curve the panel moves on. The far edge stays rounded.
  const nearRadius = useTransform(
    ctx.progress,
    [0, 0.2, 0.4, 1],
    [RADIUS, 0, 0, RADIUS],
  );
  const rotate = useTransform(ctx.progress, (p) =>
    ctx.reduce ? (ctx.open ? 180 : 0) : Math.min(1, Math.max(0, p)) * 180,
  );
  return (
    <motion.button
      type="button"
      id={ctx.triggerId}
      disabled={ctx.disabled}
      aria-haspopup="listbox"
      aria-expanded={ctx.open}
      aria-controls={ctx.listId}
      onClick={() => ctx.setOpen(!ctx.open)}
      style={{
        borderTopLeftRadius: ctx.reduce || !isTop ? RADIUS : nearRadius,
        borderTopRightRadius: ctx.reduce || !isTop ? RADIUS : nearRadius,
        borderBottomLeftRadius: ctx.reduce || isTop ? RADIUS : nearRadius,
        borderBottomRightRadius: ctx.reduce || isTop ? RADIUS : nearRadius,
      }}
      className={cn(
        "relative z-10 flex w-full items-center justify-between gap-2 border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors",
        "hover:border-(--color-border-strong) focus-visible:ring-2 focus-visible:ring-foreground/20",
        "disabled:pointer-events-none disabled:opacity-50",
        className,
      )}
    >
      {children}
      <motion.span
        aria-hidden
        style={{ rotate }}
        className="text-muted-foreground"
      >
        <ChevronDown className="h-4 w-4" />
      </motion.span>
    </motion.button>
  );
}

export interface SelectValueProps {
  placeholder?: string;
  className?: string;
}

export function SelectValue({ placeholder, className }: SelectValueProps) {
  const ctx = useSelectContext("SelectValue");
  const label = ctx.labelFor(ctx.value);
  return (
    <span
      className={cn(label ? "text-foreground" : "text-muted-foreground", className)}
    >
      {label ?? placeholder ?? "Select"}
    </span>
  );
}

export interface SelectContentProps {
  className?: string;
  children: ReactNode;
}

export function SelectContent({ className, children }: SelectContentProps) {
  const ctx = useSelectContext("SelectContent");
  const panelRef = useRef<HTMLDivElement>(null);
  const [height, setHeight] = useState(0);
  const { open, progress, reduce, setPlacement } = ctx;

  // Keep descendants laid out while hidden: display:none resets nested
  // scrollers in Chromium. Collapse only the hidden outer footprint after
  // exit so it cannot extend page scrolling. Opening restores the full box
  // before measurement or focus; neither layout change is animated.
  const [exiting, setExiting] = useState(false);
  const [wasOpen, setWasOpen] = useState(open);
  if (open !== wasOpen) {
    setWasOpen(open);
    if (!open) setExiting(true);
  }
  const present = open || exiting;
  useMotionValueEvent(progress, "animationComplete", () => {
    if (progress.get() === 0) setExiting(false);
  });

  useLayoutEffect(() => {
    const node = panelRef.current;
    if (!node || !present) return;
    const measure = () => setHeight(node.offsetHeight);
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(node);
    return () => observer.disconnect();
  }, [present]);

  // On open, flip upward when there isn't room below and there's more above.
  useLayoutEffect(() => {
    if (!open) return;
    const trigger = document.getElementById(ctx.triggerId);
    const node = panelRef.current;
    if (!trigger || !node) return;
    const rect = trigger.getBoundingClientRect();
    const h = node.offsetHeight;
    const below = window.innerHeight - rect.bottom;
    const above = rect.top;
    setPlacement(below < h + 16 && above > below ? "top" : "bottom");
  }, [open, ctx.triggerId, setPlacement]);

  const isTop = ctx.placement === "top";

  // Reveal a full-size panel from its near edge. No ancestor scales the list,
  // so rows, borders and shadow keep their natural thickness. The clip and
  // corner writes can paint; only the gap uses a transform. All read one
  // progress value, with no animated layout properties.
  const ready = height > 0 && !reduce;
  const fromY = ready ? (isTop ? GAP : -GAP) : 0;
  const detach = useTransform(progress, [0.4, 1], [0, 1]);
  const y = useTransform(detach, (d) => lerp(fromY, 0, d));
  const nearRadius = useTransform(detach, (d) => (ready ? RADIUS * d : RADIUS));
  const borderRadius = useTransform(nearRadius, (near) =>
    isTop
      ? `${RADIUS}px ${RADIUS}px ${near}px ${near}px`
      : `${near}px ${near}px ${RADIUS}px ${RADIUS}px`,
  );
  const clipPath = useTransform([progress, nearRadius], ([p, near]: number[]) => {
    if (!ready) return "none";
    // Clamp overshoot so the clip never extends into the trigger or exposes
    // overflowing children. At rest remove it so the native shadow is whole.
    const reveal = Math.min(1, Math.max(0, p));
    if (reveal === 1) return "none";
    const hidden = Math.max(0, (height - LIP) * (1 - reveal));
    const inset = isTop ? `${hidden}px 0 0 0` : `0 0 ${hidden}px 0`;
    const corners = isTop
      ? `${RADIUS}px ${RADIUS}px ${near}px ${near}px`
      : `${near}px ${near}px ${RADIUS}px ${RADIUS}px`;
    return `inset(${inset} round ${corners})`;
  });
  // The surface materialises over its first frames rather than popping in as
  // a lip; with reduced motion the fade is the whole entrance.
  const opacity = useTransform(progress, reduce ? [0, 1] : [0, 0.12], [0, 1]);

  return (
    <motion.div
      ref={panelRef}
      id={ctx.listId}
      role="listbox"
      aria-labelledby={ctx.triggerId}
      aria-hidden={!open}
      inert={!open}
      style={{
        visibility: present ? undefined : "hidden",
        maxHeight: present ? undefined : 0,
        borderWidth: present ? undefined : 0,
        y,
        clipPath,
        borderRadius,
        opacity,
        pointerEvents: open ? "auto" : "none",
        marginTop: isTop ? 0 : GAP,
        marginBottom: isTop ? GAP : 0,
      }}
      // flush against the trigger, then separates into its own rounded pill;
      // sits above or below depending on available space
      className={cn(
        "absolute left-0 right-0 z-20 overflow-hidden border border-border bg-background shadow-lg",
        isTop ? "bottom-full" : "top-full",
        className,
      )}
    >
      <motion.div
        initial={false}
        animate={open ? "show" : "hidden"}
        className="p-1"
      >
        {children}
      </motion.div>
    </motion.div>
  );
}

export interface SelectItemProps {
  value: string;
  disabled?: boolean;
  className?: string;
  children: ReactNode;
}

export function SelectItem({
  value,
  disabled = false,
  className,
  children,
}: SelectItemProps) {
  const ctx = useSelectContext("SelectItem");
  const selected = ctx.value === value;
  const label = typeof children === "string" ? children : value;

  useLayoutEffect(() => {
    ctx.register(value, label);
    return () => ctx.unregister(value);
  }, [ctx.register, ctx.unregister, value, label]);

  return (
    <motion.div variants={ctx.reduce ? undefined : ITEM_VARIANTS}>
      <button
        type="button"
        role="option"
        aria-selected={selected}
        disabled={disabled}
        onClick={() => ctx.select(value)}
        className={cn(
          "flex w-full items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-left text-sm outline-none transition-colors",
          selected
            ? "bg-muted text-foreground"
            : "text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:bg-muted",
          "disabled:pointer-events-none disabled:opacity-50",
          className,
        )}
      >
        {children}
        {selected ? <Check className="h-3.5 w-3.5 shrink-0" /> : null}
      </button>
    </motion.div>
  );
}
components/motion/tooltip/grace.ts
type Point = { x: number; y: number };
type Box = { left: number; top: number; right: number; bottom: number };

/**
 * The corridor a pointer may cross between the trigger and the open tooltip
 * without closing it: the convex hull of where it left and the box it is
 * heading for. A fixed timeout either cuts a slow diagonal move short or keeps
 * the label alive after the pointer has plainly gone elsewhere; this follows
 * the path itself.
 */
export function graceArea(exit: Point, target: Box): Point[] {
  // A few pixels either side of the exit point give the corridor some width at
  // its narrow end, so a hand that drifts sideways while it moves stays inside.
  const pad = 5;
  return hull([
    { x: exit.x - pad, y: exit.y - pad },
    { x: exit.x + pad, y: exit.y - pad },
    { x: exit.x - pad, y: exit.y + pad },
    { x: exit.x + pad, y: exit.y + pad },
    { x: target.left, y: target.top },
    { x: target.right, y: target.top },
    { x: target.right, y: target.bottom },
    { x: target.left, y: target.bottom },
  ]);
}

export function inBox(point: Point, box: Box) {
  return (
    point.x >= box.left && point.x <= box.right && point.y >= box.top && point.y <= box.bottom
  );
}

export function inPolygon(point: Point, polygon: Point[]) {
  let inside = false;
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const a = polygon[i];
    const b = polygon[j];
    if (
      a.y > point.y !== b.y > point.y &&
      point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x
    ) {
      inside = !inside;
    }
  }
  return inside;
}

// Monotone chain; the corridor is always convex, so a hull is all it needs.
function hull(points: Point[]) {
  const sorted = [...points].sort((a, b) => a.x - b.x || a.y - b.y);
  const cross = (o: Point, a: Point, b: Point) =>
    (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
  const lower: Point[] = [];
  for (const p of sorted) {
    while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
      lower.pop();
    }
    lower.push(p);
  }
  const upper: Point[] = [];
  for (let i = sorted.length - 1; i >= 0; i--) {
    const p = sorted[i];
    while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
      upper.pop();
    }
    upper.push(p);
  }
  lower.pop();
  upper.pop();
  return lower.concat(upper);
}

/**
 * How the user last reached for the page. A focus that a mouse press caused is
 * not a request to read the label: the pointer is already on the control and
 * hovering showed it. A focus that followed a key — Tab, or a roving arrow key
 * moving focus in script — is the only way a keyboard user sees it at all.
 */
let modality: "keyboard" | "pointer" = "keyboard";
let listening = false;

export function watchModality() {
  if (listening || typeof window === "undefined") return;
  listening = true;
  window.addEventListener("keydown", () => (modality = "keyboard"), true);
  window.addEventListener("pointerdown", () => (modality = "pointer"), true);
}

export function focusFromKeyboard() {
  return modality === "keyboard";
}
components/motion/tooltip/positioner.tsx
"use client";

import { useIsPresent } from "motion/react";
import { type CSSProperties, type ReactNode, type Ref, useCallback, useState } from "react";
import { type TooltipSide, useTooltipPosition } from "./use-position";

type PositionProps = Parameters<typeof useTooltipPosition>[0];

/** Readiness lives with the mounted overlay, not a trigger ref's attach/detach cycle. */
export function TooltipPositioner({
  children,
  ...position
}: Omit<PositionProps, "open" | "onPosition"> & {
  children: (ready: boolean, present: boolean, side: TooltipSide) => ReactNode;
}) {
  const present = useIsPresent();
  const [ready, setReady] = useState(false);
  const [side, setSide] = useState<TooltipSide>(position.side);
  const onPosition = useCallback((placed: TooltipSide) => {
    setReady(true);
    setSide(placed);
  }, []);
  useTooltipPosition({ ...position, open: present, onPosition });
  return (
    <span
      ref={position.floatingRef}
      inert={!present}
      aria-hidden={!present || undefined}
      className="pointer-events-none fixed left-0 top-0 z-[9999] w-max"
      style={{ visibility: "hidden", maxWidth: "calc(100vw - 16px)" }}
    >
      {children(ready, present, side)}
    </span>
  );
}

// Each arrow is a 12px box whose base sits on the surface's inner border edge,
// so its fill covers the border beneath it and only the two outer edges stroke.
const ARROW: Record<TooltipSide, { path: string; style: CSSProperties }> = {
  top: { path: "M0 6L6 12L12 6", style: { top: "calc(100% - 6px)" } },
  bottom: { path: "M0 6L6 0L12 6", style: { bottom: "calc(100% - 6px)" } },
  left: { path: "M6 0L12 6L6 12", style: { left: "calc(100% - 6px)" } },
  right: { path: "M6 0L0 6L6 12", style: { right: "calc(100% - 6px)" } },
};

export function TooltipArrow({ side, ref }: { side: TooltipSide; ref: Ref<SVGSVGElement> }) {
  const { path, style } = ARROW[side];
  return (
    <svg
      ref={ref}
      aria-hidden="true"
      width="12"
      height="12"
      viewBox="0 0 12 12"
      className="pointer-events-none absolute overflow-visible"
      style={style}
    >
      <path d={`${path}Z`} style={{ fill: "var(--tooltip-arrow-fill, transparent)" }} />
      <path
        d={path}
        fill="none"
        strokeWidth="1"
        style={{ stroke: "var(--tooltip-arrow-stroke, transparent)" }}
      />
    </svg>
  );
}
components/motion/tooltip/use-position.ts
"use client";

import {
  arrow as arrowMiddleware,
  autoUpdate,
  computePosition,
  flip,
  hide,
  offset,
  type Placement,
  shift,
  type VirtualElement,
} from "@floating-ui/dom";
import { type RefObject, useCallback, useLayoutEffect, useRef } from "react";

export type TooltipSide = "top" | "right" | "bottom" | "left";
export type TooltipAlign = "start" | "center" | "end";
export type TooltipPoint = { x: number; y: number };
export type TooltipAnchor = HTMLElement | SVGElement;

const ORIGIN: Record<TooltipSide, string> = {
  top: "center bottom",
  bottom: "center top",
  left: "right center",
  right: "left center",
};

/** Position is geometry, not animation. One write per frame, never a spring chasing a pointer. */
export function useTooltipPosition({
  open,
  anchor,
  floatingRef,
  arrowRef,
  anchorPoint,
  followCursor,
  side,
  align,
  sideOffset,
  onDismiss,
  onPosition,
  pointer,
}: {
  open: boolean;
  anchor: TooltipAnchor | null;
  floatingRef: RefObject<HTMLSpanElement | null>;
  arrowRef?: RefObject<SVGSVGElement | null>;
  anchorPoint?: TooltipPoint;
  followCursor: boolean;
  side: TooltipSide;
  align: TooltipAlign;
  sideOffset?: number;
  onDismiss: () => void;
  onPosition: (side: TooltipSide) => void;
  pointer: ReturnType<typeof useTooltipPointer>;
}) {
  const { cursor, onMove } = pointer;
  const preferred: Placement = align === "center" ? side : `${side}-${align}`;
  const cursorSide = useRef<Placement | null>(null);
  useLayoutEffect(() => {
    cursorSide.current = open ? preferred : null;
  }, [open, preferred]);
  const frame = useRef<number | null>(null);
  const version = useRef(0);
  const update = useRef<() => void>(() => {});
  const schedule = useCallback(() => {
    // Invalidate older async calculations as soon as new geometry is requested.
    version.current++;
    if (frame.current !== null) return;
    frame.current = requestAnimationFrame(() => {
      frame.current = null;
      update.current();
    });
  }, []);
  useLayoutEffect(() => {
    onMove.current = schedule;
    return () => {
      onMove.current = null;
    };
  }, [onMove, schedule]);
  const pointX = anchorPoint?.x;
  const pointY = anchorPoint?.y;

  // Latest committed inputs are read without recreating observers for every period/content update.
  useLayoutEffect(() => {
    update.current = () => {
      const floating = floatingRef.current;
      if (!open || !anchor || !floating) return;
      const revision = ++version.current;
      const currentCursor = followCursor ? cursor.current : null;
      const virtual = Boolean(currentCursor) || pointX !== undefined || pointY !== undefined;
      const reference: Element | VirtualElement = virtual
        ? {
            contextElement: anchor,
            getBoundingClientRect: () => {
              const rect = anchor.getBoundingClientRect();
              const x = currentCursor?.x ?? rect.left + rect.width * (pointX ?? 0.5);
              const y = currentCursor?.y ?? rect.top + rect.height * (pointY ?? 0.5);
              return { x, y, left: x, right: x, top: y, bottom: y, width: 0, height: 0 };
            },
          }
        : anchor;
      const arrowElement = arrowRef?.current ?? null;
      void computePosition(reference, floating, {
        strategy: "fixed",
        placement: currentCursor ? (cursorSide.current ?? preferred) : preferred,
        middleware: [
          offset(sideOffset ?? (currentCursor ? 12 : 8)),
          flip({ padding: 8 }),
          shift({ padding: 8 }),
          ...(arrowElement ? [arrowMiddleware({ element: arrowElement, padding: 10 })] : []),
          // A label left floating over whatever scrolled into its trigger's place
          // describes nothing there. Points inside a chart move with the chart.
          ...(virtual ? [] : [hide({ strategy: "referenceHidden" })]),
        ],
      }).then(({ x, y, placement, middlewareData }) => {
        if (version.current !== revision || !floating.isConnected) return;
        // Hold the chosen side for this hover session. Crossing a flip threshold
        // repeatedly must not bounce the surface above and below the pointer.
        if (currentCursor) cursorSide.current = placement;
        else cursorSide.current = null;
        const placedSide = placement.split("-")[0] as TooltipSide;
        const dpr = window.devicePixelRatio || 1;
        floating.style.transform = `translate3d(${Math.round(x * dpr) / dpr}px, ${Math.round(y * dpr) / dpr}px, 0)`;
        floating.style.setProperty("--tooltip-origin", ORIGIN[placedSide]);
        // A box with no size has no clipped state to report, only a missing layout.
        const box = anchor.getBoundingClientRect();
        const clipped =
          Boolean(middlewareData.hide?.referenceHidden) && (box.width > 0 || box.height > 0);
        floating.style.visibility = clipped ? "hidden" : "visible";
        floating.dataset.placement = placement;
        const surface = arrowElement?.parentElement;
        if (arrowElement && surface) {
          // Measured against the positioner, drawn inside the surface's border.
          const { x: ax, y: ay } = middlewareData.arrow ?? {};
          if (ax != null) arrowElement.style.left = `${ax - surface.clientLeft}px`;
          if (ay != null) arrowElement.style.top = `${ay - surface.clientTop}px`;
          // Paint with whatever the surface resolved to, so a restyled tooltip
          // keeps a matching arrow without a second set of classes.
          const style = getComputedStyle(surface);
          arrowElement.style.setProperty("--tooltip-arrow-fill", style.backgroundColor);
          arrowElement.style.setProperty("--tooltip-arrow-stroke", style.borderTopColor);
        }
        onPosition(placedSide);
      });
    };
    if (open) schedule();
  }, [
    open,
    anchor,
    floatingRef,
    arrowRef,
    followCursor,
    pointX,
    pointY,
    preferred,
    sideOffset,
    schedule,
    onPosition,
    cursor,
  ]);

  useLayoutEffect(() => {
    const floating = floatingRef.current;
    if (!open || !anchor || !floating) return;
    const stop = autoUpdate(anchor, floating, schedule);
    const onScroll = () => {
      // A stationary pointer no longer describes the same chart point after scrolling.
      if (followCursor && cursor.current) onDismiss();
    };
    window.addEventListener("scroll", onScroll, true);
    return () => {
      stop();
      window.removeEventListener("scroll", onScroll, true);
      version.current++;
      if (frame.current !== null) cancelAnimationFrame(frame.current);
      frame.current = null;
    };
  }, [open, anchor, floatingRef, followCursor, onDismiss, schedule, cursor]);

  useLayoutEffect(
    () => () => {
      version.current++;
      if (frame.current !== null) cancelAnimationFrame(frame.current);
    },
    [],
  );
}

/** Pointer lifetime belongs to the anchor, including the opening delay. */
export function useTooltipPointer(anchor: TooltipAnchor | null, followCursor: boolean) {
  const cursor = useRef<TooltipPoint | null>(null);
  const onMove = useRef<(() => void) | null>(null);
  const pointerFocus = useRef(false);
  useLayoutEffect(() => {
    if (!anchor || !followCursor) return;
    const point = (event: PointerEvent) => {
      if (event.type === "pointermove" && event.pointerType === "touch" && !event.buttons) return;
      cursor.current = { x: event.clientX, y: event.clientY };
      if (event.type === "pointerdown") pointerFocus.current = true;
      onMove.current?.();
    };
    const keyboard = () => {
      cursor.current = null;
      pointerFocus.current = false;
      onMove.current?.();
    };
    const focus = () => {
      if (!pointerFocus.current) cursor.current = null;
      pointerFocus.current = false;
      onMove.current?.();
    };
    const leave = () => {
      cursor.current = null;
    };
    anchor.addEventListener("pointerenter", point as EventListener, { passive: true });
    anchor.addEventListener("pointermove", point as EventListener, { passive: true });
    anchor.addEventListener("pointerdown", point as EventListener, { passive: true });
    anchor.addEventListener("pointerleave", leave);
    anchor.addEventListener("pointercancel", leave);
    anchor.addEventListener("keydown", keyboard);
    anchor.addEventListener("focusin", focus);
    return () => {
      cursor.current = null;
      anchor.removeEventListener("pointerenter", point as EventListener);
      anchor.removeEventListener("pointermove", point as EventListener);
      anchor.removeEventListener("pointerdown", point as EventListener);
      anchor.removeEventListener("pointerleave", leave);
      anchor.removeEventListener("pointercancel", leave);
      anchor.removeEventListener("keydown", keyboard);
      anchor.removeEventListener("focusin", focus);
    };
  }, [anchor, followCursor]);

  return { cursor, onMove };
}
components/motion/tooltip-surface.tsx
"use client";

import { motion, useReducedMotion } from "motion/react";
import type { ComponentProps, ReactNode, Ref } from "react";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";

/** Presentation only: the unanimated parent owns measurement and positioning. */
export function TooltipSurface({
  children,
  side: _side = "top",
  className,
  ref,
  ready = true,
  style,
  ...props
}: Omit<ComponentProps<typeof motion.span>, "children"> & {
  children?: ReactNode;
  /** Start the entrance only after the positioning layer has been measured. */
  ready?: boolean;
  side?: "top" | "right" | "bottom" | "left";
  ref?: Ref<HTMLSpanElement>;
}) {
  const reduce = useReducedMotion();
  const closed = { opacity: 0, scale: reduce ? 1 : 0.94 };
  return (
    <motion.span
      ref={ref}
      role="tooltip"
      initial={closed}
      animate={{
        ...(ready ? { opacity: 1, scale: 1 } : closed),
        transition: { duration: 0.18, ease: EASE_OUT },
      }}
      exit={{ ...closed, transition: { duration: 0.12, ease: EASE_OUT } }}
      style={{ transformOrigin: "var(--tooltip-origin, center)", ...style }}
      className={cn(
        "block whitespace-nowrap rounded-lg border border-border bg-background px-2.5 py-1 text-xs font-medium text-foreground shadow-lg",
        className,
      )}
      {...props}
    >
      {children}
    </motion.span>
  );
}
lib/hooks/use-dismiss.ts
"use client";

import { type RefObject, useEffect } from "react";

/**
 * What the dismissing gesture does to the control it landed on.
 *
 * `"pass-through"` is the platform norm (native popover light-dismiss): the
 * tap closes the overlay *and* activates whatever was under it. Use
 * `"consume"` where the open overlay sits over or beside controls that would
 * be costly to trigger by accident — the dismissal then swallows the
 * activation too, so the gesture only closes.
 */
export type DismissBehavior = "pass-through" | "consume";

export interface DismissOptions {
  /** Default `"pass-through"`. */
  behavior?: DismissBehavior;
  /** Dismiss on Escape as well. Default true. */
  escape?: boolean;
  /** Return true for an outside target that should *not* dismiss. Must be stable. */
  ignore?: (target: Element) => boolean;
}

/**
 * What every currently open dismiss scope counts as inside itself. A consumed
 * dismissal reads this to tell a stray gesture from one that belongs to an
 * overlay in front of it: overlays have no shared z-order to consult, but the
 * one the gesture landed in has said as much by registering it.
 */
const openScopes = new Set<(target: Element) => boolean>();

function claimedByAnotherScope(
  self: (target: Element) => boolean,
  target: Element,
) {
  for (const scope of openScopes) {
    if (scope !== self && scope(target)) return true;
  }
  return false;
}

// preventDefault on pointerdown does not suppress the click that follows, so
// consuming a gesture means swallowing that click itself. The swallower
// deliberately outlives the effect that installed it — the dismissal it
// belongs to has already unmounted or re-rendered by the time the click lands.
// It releases on that click, or on the next gesture if the pointer is dragged
// away and no click ever arrives, so it can never eat a later one. A keydown
// releases it too: a gesture that ends with neither a click nor a cancel would
// otherwise leave it armed, and the click Enter synthesizes on some focused
// control is not the one this dismissal was owed.
function consumeActivation(source: Event) {
  const swallow = (event: MouseEvent) => {
    event.preventDefault();
    event.stopPropagation();
    release();
  };
  const restart = (event: Event) => {
    if (event !== source) release();
  };
  const release = () => {
    window.removeEventListener("click", swallow, true);
    window.removeEventListener("pointerdown", restart, true);
    window.removeEventListener("pointercancel", restart, true);
    window.removeEventListener("keydown", release, true);
  };
  window.addEventListener("click", swallow, true);
  window.addEventListener("pointerdown", restart, true);
  window.addEventListener("pointercancel", restart, true);
  window.addEventListener("keydown", release, true);
}

/**
 * Close an open overlay on Escape or a pointerdown outside `ref`. Pass `null`
 * for `ref` when what counts as inside isn't one element, and say so with
 * `ignore` instead.
 *
 * The pointerdown listener is capture-phase: a bubble-phase one is blinded by
 * any handler in between that stops propagation, and an overlay cannot know
 * what it is layered over. `onDismiss` and `ignore` must be stable (wrap in
 * useCallback) so the listeners aren't re-bound every render while open.
 */
export function useDismiss(
  open: boolean,
  onDismiss: () => void,
  ref: RefObject<HTMLElement | SVGElement | null> | null,
  {
    behavior = "pass-through",
    escape: dismissOnEscape = true,
    ignore,
  }: DismissOptions = {},
) {
  useEffect(() => {
    if (!open) return;
    const inside = (target: Element) =>
      Boolean(ref?.current?.contains(target)) || Boolean(ignore?.(target));
    const onKey = (event: KeyboardEvent) => {
      if (dismissOnEscape && event.key === "Escape") onDismiss();
    };
    const onPointer = (event: PointerEvent) => {
      const target = event.target as Element | null;
      if (!target || inside(target)) return;
      // Outside this overlay, but inside one that is also open: the gesture is
      // that overlay's to answer, and swallowing its click from behind would
      // cost the user the control they actually aimed at.
      if (behavior === "consume" && !claimedByAnotherScope(inside, target)) {
        consumeActivation(event);
      }
      onDismiss();
    };
    openScopes.add(inside);
    window.addEventListener("keydown", onKey);
    window.addEventListener("pointerdown", onPointer, true);
    return () => {
      openScopes.delete(inside);
      window.removeEventListener("keydown", onKey);
      window.removeEventListener("pointerdown", onPointer, true);
    };
  }, [open, onDismiss, ref, behavior, dismissOnEscape, ignore]);
}
lib/hooks/use-hover-gesture.ts
"use client";

import { useMemo, useRef } from "react";
import { isHoveringPointer } from "@/lib/touch";

interface BoundaryEvent {
  pointerId: number;
  pointerType: string;
  buttons: number;
}

export interface HoverGesture {
  /** True when this enter starts a hover: the pointer arrived resting, not pressing. */
  enter: (event: BoundaryEvent) => boolean;
  /** True when this leave ends a hover that entered as one. */
  leave: (event: BoundaryEvent) => boolean;
}

/**
 * Pairs a surface's enter with its leave, per pointer.
 *
 * `isHoveringPointer` answers the question the *enter* asks — is this pointer
 * resting on the surface or pressing it — and both boundary cases go wrong if
 * the leave is asked the same question again:
 *
 * - A pen with no hover never rests. It arrives in contact, taps, and the spec
 *   then requires its boundary events after `pointerup`, so the leave carries
 *   `buttons: 0` and reads as a mouse gliding off. Hover teardown then undid
 *   the tap — the panel the pen had just opened closed under it.
 * - A mouse pressed on the surface and dragged off leaves with `buttons: 1`.
 *   Skipping teardown there strands the surface open: the release happens
 *   outside, and no second leave ever comes.
 *
 * So the state a hover holds is released by the pointer that took it, whatever
 * the buttons say at the boundary, and a pointer that arrived in contact never
 * took it in the first place. Contact is the exception tracked here, not
 * hover: a leave from a pointer this surface never saw enter — mounted under
 * the cursor, say — still counts, since the alternative is state with no way
 * out.
 */
export function useHoverGesture(): HoverGesture {
  const contact = useRef(new Set<number>());

  return useMemo(
    () => ({
      enter: (event) => {
        if (isHoveringPointer(event)) {
          contact.current.delete(event.pointerId);
          return true;
        }
        contact.current.add(event.pointerId);
        return false;
      },
      leave: (event) => {
        const arrivedInContact = contact.current.delete(event.pointerId);
        return !arrivedInContact && event.pointerType !== "touch";
      },
    }),
    [],
  );
}
lib/hooks/use-tap-gesture.ts
"use client";

import { useMemo, useRef } from "react";

/** What a pointerdown recorded, read back by the click that ends its gesture. */
export interface TapRecord<S> {
  /** Which input started the gesture. */
  pointerType: string;
  /** What the surface was showing when it started. */
  state: S;
}

export interface TapGesture<S> {
  /** Record the gesture a pointerdown starts, with the state it starts in. */
  start: (event: { pointerType: string }, state: S) => void;
  /** Read the record and clear it. `null` when no pointer is behind this click. */
  take: () => TapRecord<S> | null;
  /** Drop the record: this gesture will never spend it on a click. */
  drop: () => void;
}

/**
 * The pointer gesture behind a click, recorded where the click cannot report
 * it. A `click` carries no `pointerType` in the engines that matter, so the
 * `pointerdown` before it is the only thing that says which input activated
 * the control — and whether one did at all, since keyboard activation
 * synthesizes a click with no pointer behind it.
 *
 * State goes in with the record because a click reports that no better: a
 * browser that focuses a control on contact can open the very panel the tap
 * was meant to open, and reading "is it open" at click time then undoes it.
 * What the gesture started against is what it acts on.
 *
 * The record is spent by one click and dropped by everything else, because a
 * record that outlives its gesture is worse than none:
 *
 * - A scroll or an OS gesture takes the touch away — `pointercancel`, no click
 *   ever — and the finger would sit in the record until some later click.
 * - That later click is often `Enter` on a keyboard, which arrives with no
 *   pointerdown of its own and would inherit the abandoned finger. A keydown
 *   is the start of a keyboard activation and never part of a tap, so it drops
 *   the record too.
 *
 * Both ends have to be wired by the surface: `drop` on `onPointerCancel` and
 * on `onKeyDown`.
 */
export function useTapGesture<S>(): TapGesture<S> {
  const record = useRef<TapRecord<S> | null>(null);

  return useMemo(
    () => ({
      start: (event, state) => {
        record.current = { pointerType: event.pointerType, state };
      },
      take: () => {
        const spent = record.current;
        record.current = null;
        return spent;
      },
      drop: () => {
        record.current = null;
      },
    }),
    [],
  );
}
components/motion/popover-position.ts
"use client";

import {
  type MutableRefObject,
  useCallback,
  useLayoutEffect,
  useState,
} from "react";

export type PortalLayout = {
  trigger: {
    left: number;
    top: number;
    width: number;
    height: number;
  };
  content: {
    width: number;
    height: number;
  };
};

function sameLayout(a: PortalLayout | null, b: PortalLayout) {
  return (
    a?.trigger.left === b.trigger.left &&
    a.trigger.top === b.trigger.top &&
    a.trigger.width === b.trigger.width &&
    a.trigger.height === b.trigger.height &&
    a.content.width === b.content.width &&
    a.content.height === b.content.height
  );
}

/** Measures a trigger and portalled panel in viewport coordinates. */
export function usePopoverPortalPosition<
  TriggerElement extends HTMLElement,
  ContentElement extends HTMLElement,
>(
  triggerRef: MutableRefObject<TriggerElement | null>,
  contentRef: MutableRefObject<ContentElement | null>,
  active: boolean,
) {
  const [layout, setLayout] = useState<PortalLayout | null>(null);

  const update = useCallback(() => {
    const trigger = triggerRef.current;
    const content = contentRef.current;
    if (!trigger || !content) return;

    const rect = trigger.getBoundingClientRect();
    const next: PortalLayout = {
      trigger: {
        left: rect.left,
        top: rect.top,
        width: rect.width,
        height: rect.height,
      },
      content: {
        width: content.offsetWidth,
        height: content.offsetHeight,
      },
    };
    setLayout((current) => (sameLayout(current, next) ? current : next));
  }, [contentRef, triggerRef]);

  useLayoutEffect(() => {
    update();
    if (!active) return;

    const trigger = triggerRef.current;
    const content = contentRef.current;
    const observer = new ResizeObserver(update);
    if (trigger) observer.observe(trigger);
    if (content) observer.observe(content);

    window.addEventListener("scroll", update, true);
    window.addEventListener("resize", update);
    return () => {
      observer.disconnect();
      window.removeEventListener("scroll", update, true);
      window.removeEventListener("resize", update);
    };
  }, [active, contentRef, triggerRef, update]);

  return layout;
}
lib/touch.ts
// Shared touch primitives. iOS and iPadOS run their own gestures on top of the
// page — the long-press selection callout and the selection it drags in with
// it — and they win: once the platform claims a touch it cancels ours
// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own
// their gesture have to opt out.
//
// What the two classes below cover, precisely:
// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:
//   it is not a property other engines have, so it is inert everywhere else.
// - `user-select: none` stops the long-press selection on every engine,
//   Android included, and stops a drag from painting a selection under the
//   cursor. It is inherited, so it reaches every descendant — which is why the
//   two classes differ only in whether they apply it unconditionally.
// What neither covers:
// - Chrome for Android's long-press menu on a link or an image. No CSS
//   suppresses it; a gesture surface that wraps one needs its own
//   `onContextMenu` with `preventDefault()`.
// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is
//   not inherited and plain divs and buttons are not drag sources, so setting
//   it on the surface does nothing — the child itself needs `draggable={false}`.

/**
 * Classes for a surface that *is* the control: a thumb, a drum, a stage, a
 * handle, a hold button. Selection is suppressed on every input, because a
 * drag that highlights the control's own label is wrong on a mouse too.
 * Compose with `touch-none` when the surface also owns the scroll axis — leave
 * it off when the page must still scroll from there.
 */
export const TOUCH_GESTURE_CLASS = "select-none [-webkit-touch-callout:none]";

/**
 * The same opt-out for a gesture surface that wraps content the consumer owns:
 * a scroller, a context-menu trigger, a sheet header, a list row. Selection is
 * suppressed only where the platform runs its own press gestures — a coarse
 * pointer — so a mouse user can still select and copy that content. If the
 * gesture itself would paint a selection under the cursor, add `select-none`
 * for the duration of the gesture rather than reaching for
 * `TOUCH_GESTURE_CLASS`.
 *
 * `pointer: coarse` describes the *primary* pointer and nothing else, so a
 * hybrid machine reads it wrong in both directions: a tablet with a mouse
 * plugged in keeps touch as primary and loses mouse selection, and a laptop
 * with a touchscreen keeps the mouse as primary and leaves selection live
 * under a finger. No media query can answer per interaction — the query is
 * about the device, and the question is about the gesture in progress. The
 * default stays here because it is right on the machines that are one thing or
 * the other, and losing a selection is a nuisance; where the miss costs a
 * *gesture* instead, the surface pairs it with `holdSelection` on the press.
 */
export const TOUCH_GESTURE_CONTENT_CLASS =
  "[-webkit-touch-callout:none] pointer-coarse:select-none";

/**
 * Suppress selection on `element` for as long as a gesture is running on it,
 * whatever the primary pointer of the machine happens to be. Returns the
 * release. Inline, so it wins over the class above and is gone again the
 * moment the gesture ends.
 *
 * For the press gestures a native selection would otherwise steal — a
 * long-press that opens a menu. Elsewhere prefer the classes: a surface that
 * takes selection away for the whole session is a surface whose text nobody
 * can copy.
 */
export function holdSelection(element: HTMLElement) {
  element.style.setProperty("user-select", "none");
  element.style.setProperty("-webkit-user-select", "none");
  return () => {
    element.style.removeProperty("user-select");
    element.style.removeProperty("-webkit-user-select");
  };
}

/**
 * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer
 * is already gone by the time the handler runs — routine on iOS, where the
 * system can claim the touch first — and an uncaught throw takes the rest of
 * the handler, the gesture included, down with it. Touch pointers carry
 * implicit capture anyway, so losing it is never fatal.
 */
export function capturePointer(element: Element, pointerId: number) {
  try {
    element.setPointerCapture(pointerId);
  } catch {
    // Pointer is no longer active — implicit capture still applies on touch.
  }
}

/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */
export function releasePointer(element: Element, pointerId: number) {
  try {
    if (element.hasPointerCapture(pointerId)) {
      element.releasePointerCapture(pointerId);
    }
  } catch {
    // Capture was already dropped by the browser.
  }
}

/**
 * Whether this event came from a pointer that is *hovering*: not a touch, and
 * not currently pressed. Which input the user is holding right now is not
 * something a device capability can answer — a touchscreen laptop hovers and
 * taps, and iPadOS reports a fine hovering pointer for a finger — so both
 * paths stay live and each handler branches on the event it was given.
 *
 * A pen resting on the glass is making contact, not hovering: `buttons` is the
 * tell, and it sends a pen tap down the same route a finger takes.
 *
 * This answers what an *enter* asks. A leave is the other half of a pair and
 * has to be read against the enter that started it — `useHoverGesture` in
 * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it
 * rather than asking this question twice.
 */
export const isHoveringPointer = (event: {
  pointerType: string;
  buttons: number;
}) => event.pointerType !== "touch" && event.buttons === 0;

More from beui

Browse all components in this library →