"use client";
import { useEffect, useId, useRef, useState } from "react";
import {
motion,
useMotionValue,
useSpring,
useTransform,
useVelocity,
} from "motion/react";
import { cn } from "@/lib/utils";
export type SegmentedIndicator = "solid" | "outline" | "underline";
export type SegmentedSizing = "auto" | "equal";
export type SegmentedDensity = "comfortable" | "compact";
export interface SegmentedItem {
id: string;
label: string;
}
export interface SegmentedProps {
items?: SegmentedItem[];
value?: string;
defaultValue?: string;
onValueChange?: (id: string) => void;
indicator?: SegmentedIndicator;
sizing?: SegmentedSizing;
density?: SegmentedDensity;
radius?: number;
stiffness?: number;
damping?: number;
smear?: number;
glow?: boolean;
accentColor?: string;
reducedMotion?: boolean;
"aria-label"?: string;
className?: string;
}
const SPEED_REF = 5.5;
export default function Segmented({
items = [],
value,
defaultValue,
onValueChange,
indicator = "solid",
sizing = "auto",
density = "comfortable",
radius = 8,
stiffness = 520,
damping = 34,
smear = 0.5,
glow = true,
accentColor = "#a855f7",
reducedMotion = false,
"aria-label": ariaLabel = "View",
className,
}: SegmentedProps) {
const uid = useId();
const [self, setSelf] = useState(defaultValue ?? items[0]?.id ?? "");
const controlled = value !== undefined;
const active = controlled ? value : self;
const activeIndex = Math.max(
0,
items.findIndex((item) => item.id === active),
);
const compact = density === "compact";
const bar = indicator === "underline";
const position = useMotionValue(activeIndex);
const settled = useSpring(position, { stiffness, damping });
const velocity = useVelocity(settled);
useEffect(() => {
position.set(activeIndex);
}, [activeIndex, position]);
const stretch = useTransform(velocity, (v: number) =>
reducedMotion ? 0 : Math.min(1, Math.abs(v) / SPEED_REF),
);
const scaleX = useTransform(stretch, (s: number) => 1 + smear * s * 0.35);
const scaleY = useTransform(stretch, (s: number) => 1 - smear * s * 0.22);
const originX = useTransform(velocity, (v: number) => (v > 0 ? 1 : 0));
const listRef = useRef<HTMLDivElement>(null);
const select = (id: string) => {
if (!controlled) setSelf(id);
onValueChange?.(id);
};
const onKeyDown = (event: React.KeyboardEvent) => {
if (event.key !== "Home" && event.key !== "End") return;
const next = event.key === "Home" ? items[0] : items[items.length - 1];
if (!next) return;
event.preventDefault();
select(next.id);
listRef.current
?.querySelectorAll<HTMLInputElement>('input[type="radio"]')
?.[event.key === "Home" ? 0 : items.length - 1]?.focus();
};
const layoutTransition = reducedMotion
? { duration: 0 }
: { type: "spring" as const, stiffness, damping };
return (
<div
ref={listRef}
role="radiogroup"
aria-label={ariaLabel}
onKeyDown={onKeyDown}
className={cn(
"inline-flex select-none items-stretch border border-hairline bg-panel",
compact ? "gap-0.5 p-0.5" : "gap-1 p-1",
sizing === "equal" && "w-full",
className,
)}
style={{ borderRadius: radius + (compact ? 2 : 4) }}
>
{items.map((item) => {
const on = item.id === active;
return (
<label
key={item.id}
className={cn(
"relative flex cursor-pointer items-center justify-center transition-colors",
compact ? "px-2.5 py-1" : "px-3.5 py-1.5",
sizing === "equal" ? "flex-1 basis-0" : "flex-none",
!on && "hover:bg-raised/70",
)}
style={{ borderRadius: radius }}
>
<input
type="radio"
name={uid}
value={item.id}
checked={on}
onChange={() => select(item.id)}
className="peer absolute inset-0 size-full cursor-pointer opacity-0"
/>
<span
aria-hidden
className="pointer-events-none absolute inset-0 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-accent"
style={{ borderRadius: radius }}
/>
{on ? (
<motion.span
aria-hidden
layoutId={`${uid}-indicator`}
transition={layoutTransition}
className={cn(
"pointer-events-none absolute",
bar ? "inset-x-1.5 bottom-0 h-[2px]" : "inset-0",
)}
style={{ borderRadius: bar ? 2 : radius }}
>
{glow ? (
<span
className="absolute -inset-1 rounded-[inherit] blur-md"
style={{ backgroundColor: accentColor, opacity: 0.28 }}
/>
) : null}
<motion.span
className="absolute inset-0 block rounded-[inherit]"
style={{
scaleX,
scaleY,
originX,
backgroundColor:
indicator === "outline"
? `${accentColor}1f`
: accentColor,
border:
indicator === "outline"
? `1.5px solid ${accentColor}`
: undefined,
}}
/>
</motion.span>
) : null}
<span
className={cn(
"relative z-10 font-display tracking-[0.12em] uppercase",
compact ? "text-[10px]" : "text-[11px]",
"transition-colors duration-200 [transition-delay:90ms]",
on && indicator === "solid"
? "text-on-accent"
: on
? "text-accent"
: "text-ink-dim",
)}
>
{item.label}
</span>
</label>
);
})}
</div>
);
}