import React, { useState } from "react"; import { Pressable, Text, View, type LayoutChangeEvent } from "react-native"; import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { usePalette } from "../utils/palette"; export interface SegmentedOption { key: T; label: string; /** When false the segment is shown but not selectable (e.g. "coming soon"). */ disabled?: boolean; /** * Optional glyph shown left of the label. * * A render function rather than a node so the icon can take the segment's * current foreground colour — the same reason the label's colour is applied * inline. A fixed-colour node would go invisible against the indicator on * whichever of the two states it was not drawn for. */ icon?: (color: string) => React.ReactNode; } export interface SegmentedToggleProps { value: T; options: SegmentedOption[]; onChange: (next: T) => void; /** Visual size — `sm` is used inside dialogs, `default` on the auth screen. */ size?: "sm" | "default"; className?: string; } /** * Themed segmented control with a sliding indicator (reanimated). Mirrors the * website/simple-finance "Sign In / Sign Up" pill, but palette-driven so it * tracks the active theme instead of hard-coded brand colors. Shared by the * consolidated AuthScreen mode switch and the ConnectionSettings run-mode pills. * * Colors are applied via inline `style` (not className) on the Text so Android's * NativeWind doesn't override the active/inactive foreground — see the * android-nativewind-styling note. */ export function SegmentedToggle({ value, options, onChange, size = "default", className, }: SegmentedToggleProps) { const { palette: p } = usePalette(); const [trackW, setTrackW] = useState(0); const slide = useSharedValue(0); const count = options.length; const index = Math.max( 0, options.findIndex((o) => o.key === value) ); const pad = 4; const segW = trackW > 0 ? (trackW - pad * 2) / count : 0; // Drive the indicator off the selected index whenever it changes. slide.value = withTiming(index * segW, { duration: 240, easing: Easing.out(Easing.cubic) }); const indicatorStyle = useAnimatedStyle(() => ({ width: segW, transform: [{ translateX: slide.value }], })); const vPad = size === "sm" ? 9 : 11; const fontSize = size === "sm" ? 14 : 15; return ( setTrackW(e.nativeEvent.layout.width)} > {options.map((opt) => { const active = opt.key === value; return ( !opt.disabled && onChange(opt.key)} style={{ flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: opt.icon ? 7 : 0, paddingVertical: vPad, opacity: opt.disabled ? 0.45 : 1, }} > {opt.icon?.(active ? p.primaryForeground : p.mutedForeground)} {opt.label} ); })} ); } export default SegmentedToggle;