"use client"; import { arc, LayoutGroup, motion, type Transition, useReducedMotion } from "motion/react"; import { type ComponentPropsWithRef, createContext, useContext, useId, useState } from "react"; import { SPRING_LAYOUT, SPRING_PRESS } from "@/lib/ease"; import { cn } from "@/lib/utils"; // The selection ring hops to the next swatch over a low arc instead of // sliding. arc() bends with distance (apex at half of strength x distance): // ~6px to a neighbour, under 40px across a row of eight. Automatic direction // puts a horizontal bulge below the row; a negative strength mirrors it above // in both directions of travel. const SELECTION_HOP: Transition = { ...SPRING_LAYOUT, layout: { ...SPRING_LAYOUT, path: arc({ strength: -0.2 }) }, }; type ColorSelectorContextValue = { value: string; select: (value: string) => void; name: string; disabled: boolean; required: boolean; form?: string; }; const ColorSelectorContext = createContext(null); export interface ColorSelectorProps extends Omit, "onChange" | "defaultValue"> { value?: string; defaultValue?: string; onValueChange?: (value: string) => void; /** The radio group name used in form submission. Generated when omitted. */ name?: string; required?: boolean; } /** A single color choice. Give it a ColorSelectorLabel or an aria-label. */ export function ColorSelector({ value, defaultValue = "", onValueChange, name, disabled = false, required = false, form, className, children, ...props }: ColorSelectorProps) { const id = useId(); const [internal, setInternal] = useState(defaultValue); const current = value ?? internal; return ( { if (next === current) return; if (value === undefined) setInternal(next); onValueChange?.(next); }, name: name ?? id, disabled, required, form, }}>
{children}
); } export type ColorSelectorLabelProps = ComponentPropsWithRef<"legend">; export function ColorSelectorLabel({ className, ...props }: ColorSelectorLabelProps) { return ; } export type ColorSelectorListProps = ComponentPropsWithRef<"div">; export function ColorSelectorList({ className, ...props }: ColorSelectorListProps) { return
; } export interface ColorSelectorItemProps extends Omit, "type" | "value" | "defaultValue" | "checked" | "defaultChecked" | "name" | "children" | "size"> { value: string; /** Any CSS color, including a custom property. */ color: string; /** Accessible color name; selection never relies on color alone. */ label: string; /** Applied to the visible swatch surface. */ className?: string; } export function ColorSelectorItem({ value, color, label, disabled = false, className, style, onChange, ...props }: ColorSelectorItemProps) { const context = useContext(ColorSelectorContext); const reduce = useReducedMotion(); if (!context) throw new Error("ColorSelectorItem must be used within ColorSelector"); const selected = context.value === value; const unavailable = disabled || context.disabled; return ( { onChange?.(event); if (!event.defaultPrevented) context.select(value); }} className="peer sr-only" /> ); }