{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "segmented-control", "type": "registry:ui", "title": "Segmented Control", "description": "iOS-style segmented option switcher.", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/text.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json" ], "files": [ { "path": "registry/shared/ui/segmented-control.tsx", "type": "registry:ui", "target": "components/ui/segmented-control.tsx", "content": "import { useEffect, useRef, useState } from 'react';\nimport {\n Animated,\n Platform,\n Pressable,\n StyleSheet,\n Text as RNText,\n View,\n type ViewProps,\n} from 'react-native';\nimport { cn, useThemeColor } from '@/lib/utils';\n\nexport interface SegmentedControlProps extends ViewProps {\n options: readonly T[];\n value?: T;\n defaultValue?: T;\n onValueChange?: (value: T) => void;\n className?: string;\n disabled?: boolean;\n}\n\nconst PAD = 3;\n\nexport function SegmentedControl({\n options,\n value: controlledValue,\n defaultValue,\n onValueChange,\n className,\n disabled = false,\n style,\n ...props\n}: SegmentedControlProps) {\n const [uncontrolledValue, setUncontrolledValue] = useState(\n defaultValue ?? options[0]\n );\n const isControlled = controlledValue !== undefined;\n const value = isControlled ? controlledValue : uncontrolledValue;\n\n const [containerWidth, setContainerWidth] = useState(0);\n const colors = useThemeColor();\n\n const activeIndex = Math.max(0, options.indexOf(value));\n const segmentWidth =\n containerWidth > 0\n ? (containerWidth - PAD * 2) / options.length\n : 0;\n\n const translateX = useRef(new Animated.Value(0)).current;\n\n useEffect(() => {\n if (segmentWidth > 0) {\n Animated.spring(translateX, {\n toValue: activeIndex * segmentWidth,\n damping: 24,\n stiffness: 280,\n mass: 0.8,\n useNativeDriver: true,\n }).start();\n }\n }, [activeIndex, segmentWidth, translateX]);\n\n const handleSelect = (opt: T) => {\n if (disabled || opt === value) return;\n if (!isControlled) {\n setUncontrolledValue(opt);\n }\n onValueChange?.(opt);\n };\n\n return (\n setContainerWidth(e.nativeEvent.layout.width)}\n accessibilityRole=\"radiogroup\"\n {...props}\n >\n {/* Sliding Active Indicator Pill */}\n {segmentWidth > 0 && (\n \n )}\n\n {/* Segment Option Buttons with Guaranteed Equal Widths & Centered Text */}\n {options.map((opt) => {\n const active = opt === value;\n return (\n handleSelect(opt)}\n style={[\n styles.segmentButton,\n segmentWidth > 0 && { width: segmentWidth, flex: 0 },\n ]}\n >\n \n {opt}\n \n \n );\n })}\n \n );\n}\n\nconst styles = StyleSheet.create({\n indicator: {\n position: 'absolute',\n top: PAD,\n bottom: PAD,\n left: PAD,\n borderRadius: 9,\n borderWidth: StyleSheet.hairlineWidth,\n ...Platform.select({\n ios: {\n shadowColor: '#000000',\n shadowOffset: { width: 0, height: 1 },\n shadowOpacity: 0.08,\n shadowRadius: 2,\n },\n android: {\n elevation: 1.5,\n },\n default: {},\n }),\n },\n segmentButton: {\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n paddingVertical: 7,\n paddingHorizontal: 2,\n zIndex: 1,\n },\n segmentText: {\n fontSize: 12,\n textAlign: 'center',\n width: '100%',\n },\n});\n" } ] }