"use client" import * as React from "react" import { cva } from "class-variance-authority" import { cn } from "../../lib/utils" export interface FilterChipOption { value: T label: string icon?: string /** Optional count badge (catalog tier totals, view counts). */ count?: number } const filterChipVariants = cva( "inline-flex shrink-0 cursor-pointer select-none items-center rounded-full border font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", { variants: { variant: { brand: "", muted: "", }, size: { sm: "h-7 gap-1 px-2.5 text-xs", default: "h-8 gap-1.5 px-3 text-sm", }, selected: { true: "", false: "", }, }, compoundVariants: [ { variant: "brand", selected: true, class: "border-brand bg-brand text-brand-foreground hover:bg-brand/90", }, { variant: "brand", selected: false, class: "border-border bg-card text-foreground hover:border-brand/40 hover:bg-interactive-hover-soft", }, { variant: "muted", selected: true, class: "border-brand/35 bg-brand-tint font-medium text-foreground hover:bg-brand-tint/90", }, { variant: "muted", selected: false, class: "border-border bg-card text-foreground hover:border-brand/30 hover:bg-interactive-hover-soft", }, ], defaultVariants: { variant: "brand", size: "default", selected: false, }, }, ) function countClass(variant: "brand" | "muted", selected: boolean): string { if (variant === "brand" && selected) return "text-brand-foreground/90" return "text-muted-foreground" } export interface FilterChipGroupProps { value: T onValueChange: (value: T) => void options: readonly FilterChipOption[] variant?: "brand" | "muted" size?: "sm" | "default" "aria-label": string className?: string } /** * Wrapping pill filter — catalog tier chips (`variant="brand"`) and in-content * family pickers like chart types (`variant="muted"`). */ export function FilterChipGroup({ value, onValueChange, options, variant = "brand", size = "default", "aria-label": ariaLabel, className, }: FilterChipGroupProps) { const itemRefs = React.useRef<(HTMLButtonElement | null)[]>([]) React.useLayoutEffect(() => { itemRefs.current = itemRefs.current.slice(0, options.length) }, [options.length]) const focusIndex = React.useCallback( (index: number) => { const len = options.length if (len === 0) return const i = ((index % len) + len) % len onValueChange(options[i]!.value) requestAnimationFrame(() => itemRefs.current[i]?.focus()) }, [onValueChange, options], ) const onKeyDown = React.useCallback( (event: React.KeyboardEvent, index: number) => { const len = options.length if (len === 0) return if (event.key === "ArrowRight" || event.key === "ArrowDown") { event.preventDefault() focusIndex(index + 1) } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") { event.preventDefault() focusIndex(index - 1) } else if (event.key === "Home") { event.preventDefault() focusIndex(0) } else if (event.key === "End") { event.preventDefault() focusIndex(len - 1) } }, [focusIndex, options.length], ) return (
{options.map((option, index) => { const selected = option.value === value return (