import React, { createContext, useContext, useLayoutEffect, useRef, useState } from 'react' import { cn } from '../utils/cn' // ─── Primary Pill Tabs ──────────────────────────────────────────────────────── const PillTabsContext = createContext<{ value: string onChange: (value: string) => void } | null>(null) function usePillTabsContext() { const ctx = useContext(PillTabsContext) if (!ctx) throw new Error('PillTab must be used inside ') return ctx } export const PillTabs: React.FC<{ defaultValue?: string value?: string children: React.ReactNode onValueChange?: (value: string) => void }> = ({ defaultValue, value: controlledValue, children, onValueChange }) => { const [internalValue, setInternalValue] = useState(defaultValue ?? '') const isControlled = controlledValue !== undefined const value = isControlled ? controlledValue : internalValue const handleChange = (newValue: string) => { if (!isControlled) setInternalValue(newValue) onValueChange?.(newValue) } return ( {children} ) } export const PillTabsList: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '', }) => { const { value } = usePillTabsContext() const containerRef = useRef(null) const [indicator, setIndicator] = useState<{ left: number; width: number } | null>(null) useLayoutEffect(() => { const el = containerRef.current if (!el) return const updateIndicator = () => { const active = el.querySelector('[data-active="true"]') if (!active) return const cRect = el.getBoundingClientRect() const aRect = active.getBoundingClientRect() if (aRect.width === 0) return setIndicator({ left: aRect.left - cRect.left, width: aRect.width }) } updateIndicator() const ro = new ResizeObserver(updateIndicator) ro.observe(el) return () => ro.disconnect() }, [value]) return (
{indicator && (
)} {children}
) } export const PillTab: React.FC<{ value: string children: React.ReactNode className?: string }> = ({ value, children, className = '' }) => { const { value: activeValue, onChange } = usePillTabsContext() const isActive = activeValue === value return ( ) } // ─── Neutral Pill Tabs ──────────────────────────────────────────────────────── const NeutralPillTabsContext = createContext<{ value: string onChange: (value: string) => void } | null>(null) function useNeutralPillTabsContext() { const ctx = useContext(NeutralPillTabsContext) if (!ctx) throw new Error('NeutralPillTab must be used inside ') return ctx } export const NeutralPillTabs: React.FC<{ defaultValue?: string value?: string children: React.ReactNode onValueChange?: (value: string) => void }> = ({ defaultValue, value: controlledValue, children, onValueChange }) => { const [internalValue, setInternalValue] = useState(defaultValue ?? '') const isControlled = controlledValue !== undefined const value = isControlled ? controlledValue : internalValue const handleChange = (newValue: string) => { if (!isControlled) setInternalValue(newValue) onValueChange?.(newValue) } return ( {children} ) } export const NeutralPillTabsList: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '', }) => { const { value } = useNeutralPillTabsContext() const containerRef = useRef(null) const [indicator, setIndicator] = useState<{ left: number; width: number } | null>(null) useLayoutEffect(() => { const el = containerRef.current if (!el) return const updateIndicator = () => { const active = el.querySelector('[data-active="true"]') if (!active) return const cRect = el.getBoundingClientRect() const aRect = active.getBoundingClientRect() if (aRect.width === 0) return setIndicator({ left: aRect.left - cRect.left, width: aRect.width }) } updateIndicator() const ro = new ResizeObserver(updateIndicator) ro.observe(el) return () => ro.disconnect() }, [value]) return (
{indicator && (
)} {children}
) } export const NeutralPillTab: React.FC<{ value: string children: React.ReactNode className?: string }> = ({ value, children, className = '' }) => { const { value: activeValue, onChange } = useNeutralPillTabsContext() const isActive = activeValue === value return ( ) } export type TabItem = { value: string; label: string }