{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "switch", "type": "registry:ui", "title": "Switch", "description": "Animated toggle built on RN Animated (no reanimated dep).", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json" ], "files": [ { "path": "registry/shared/ui/switch.tsx", "type": "registry:ui", "target": "components/ui/switch.tsx", "content": "import { useContext, useEffect, useRef, useState } from 'react';\nimport {\n Animated,\n type GestureResponderEvent,\n Platform,\n Pressable,\n type PressableProps,\n StyleSheet,\n type ViewStyle,\n} from 'react-native';\nimport { cn, FormFieldContext, useThemeColor } from '@/lib/utils';\n\nconst SIZES = {\n default: { trackWidth: 46, trackHeight: 26, thumbSize: 22, pad: 2 },\n sm: { trackWidth: 36, trackHeight: 20, thumbSize: 16, pad: 2 },\n} as const;\n\nexport interface SwitchProps extends Omit {\n checked?: boolean;\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean) => void;\n /** Mirrors shadcn `aria-invalid`. Auto-detected from FormField error. */\n invalid?: boolean;\n size?: keyof typeof SIZES;\n className?: string;\n thumbClassName?: string;\n}\n\nexport function Switch({\n checked: controlledChecked,\n defaultChecked = false,\n onCheckedChange,\n invalid,\n size = 'default',\n className,\n thumbClassName,\n disabled = false,\n style,\n onPress,\n ...props\n}: SwitchProps) {\n const [uncontrolledChecked, setUncontrolledChecked] =\n useState(defaultChecked);\n const isControlled = controlledChecked !== undefined;\n const checked = isControlled ? controlledChecked : uncontrolledChecked;\n\n const { trackWidth, trackHeight, thumbSize, pad } = SIZES[size];\n const maxTranslate = trackWidth - thumbSize - pad * 2;\n\n const translateAnim = useRef(\n new Animated.Value(checked ? maxTranslate : 0)\n ).current;\n\n const field = useContext(FormFieldContext);\n const colors = useThemeColor();\n const isInvalid = invalid ?? !!field?.error;\n\n useEffect(() => {\n Animated.timing(translateAnim, {\n toValue: checked ? maxTranslate : 0,\n duration: 180,\n useNativeDriver: true,\n }).start();\n }, [checked, maxTranslate, translateAnim]);\n\n const handlePress = (e: GestureResponderEvent) => {\n if (disabled) return;\n const next = !checked;\n if (!isControlled) {\n setUncontrolledChecked(next);\n }\n onCheckedChange?.(next);\n onPress?.(e);\n };\n\n const trackBg = isInvalid\n ? colors.destructive\n : checked\n ? colors.primary\n : colors.input || colors.border;\n\n const dynamicTrackStyle: ViewStyle = {\n width: trackWidth,\n height: trackHeight,\n borderRadius: trackHeight / 2,\n padding: pad,\n backgroundColor: trackBg,\n borderColor: isInvalid ? colors.destructive : 'transparent',\n borderWidth: isInvalid ? 1 : 0,\n opacity: disabled ? 0.5 : 1,\n };\n\n return (\n \n \n \n );\n}\n\nconst styles = StyleSheet.create({\n trackBase: {\n justifyContent: 'center',\n },\n thumbBase: {\n ...Platform.select({\n ios: {\n shadowColor: '#000000',\n shadowOffset: { width: 0, height: 2 },\n shadowOpacity: 0.2,\n shadowRadius: 2.5,\n },\n android: {\n elevation: 2,\n },\n default: {},\n }),\n },\n});\n" } ] }