jengacore components · @jengacore/segmented
Segmented
Compact single choice that acts at once: USD / SLSH, All / Mine / Unassigned. A radio group with arrow keys.
Install
terminal
npx shadcn@latest add @jengacore/segmentedWithout the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/segmented.json
Registry dependencies
Source
components/jc/segmented.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export type SegmentOption<T extends string> = { value: T; label: React.ReactNode; count?: number }
/**
* Compact single choice for filters and switches that act at once: USD / SLSH, All / Mine / Unassigned,
* Cash / Mobile money / Transfer. A radio group, so arrow keys move between options.
* Use Tabs to switch views of the same object, the sidebar to switch between objects.
*/
export function Segmented<T extends string>({
value,
onValueChange,
options,
label,
size = "default",
className,
}: {
value: T
onValueChange: (value: T) => void
options: SegmentOption<T>[]
/** Accessible name of the group: "Currency", "Queue filter". */
label: string
size?: "default" | "lg"
className?: string
}) {
const refs = React.useRef<(HTMLButtonElement | null)[]>([])
const move = (from: number, delta: number) => {
const next = (from + delta + options.length) % options.length
const option = options[next]
if (!option) return
onValueChange(option.value)
refs.current[next]?.focus()
}
return (
<div
role="radiogroup"
aria-label={label}
data-slot="segmented"
className={cn("inline-flex gap-0.5 rounded-[7px] bg-muted p-[3px] contrast:outline contrast:outline-input", size === "lg" ? "h-9" : "h-[30px]", className)}
>
{options.map((o, i) => {
const on = o.value === value
return (
<button
key={o.value}
ref={(el) => {
refs.current[i] = el
}}
type="button"
role="radio"
aria-checked={on}
tabIndex={on ? 0 : -1}
onClick={() => onValueChange(o.value)}
onKeyDown={(e) => {
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
e.preventDefault()
move(i, 1)
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
e.preventDefault()
move(i, -1)
}
}}
className={cn(
"inline-flex items-center gap-1.5 rounded-[5px] px-3 text-[13px] whitespace-nowrap text-muted-foreground transition-colors duration-fast ease-jc hover:text-foreground",
"focus-ring [--focus-offset:1px]",
on && "pressed-state"
)}
>
{o.label}
{o.count !== undefined ? <span className="font-mono text-[11px] text-muted-foreground">{o.count}</span> : null}
</button>
)
})}
</div>
)
}