import React, { useEffect, useId, useMemo, useRef, useState } from 'react'; import { Animated, Easing, Text, View, type LayoutChangeEvent, type TextStyle } from 'react-native'; import Svg, { Defs, LinearGradient, Stop, Text as SvgText } from 'react-native-svg'; const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient); // Mirrors the web `PhaseShimmerText` palette (slate base + lighter highlight) // so the native thinking text reads identically in both light and dark mode. const SHIMMER_BASE = '#64748b'; const SHIMMER_HIGHLIGHT = '#bbc4cf'; // Matches the web `phase-shimmer` cadence (2.8s linear, see tailwind.config.js). const SHIMMER_DURATION_MS = 2800; /** * A shimmering text label. React Native has no CSS `background-clip: text`, so * we render the text as SVG filled with a horizontal gradient and sweep the * gradient across it via an animated `x1`/`x2`. An invisible RN `` drives * layout/measurement; the SVG is overlaid on top at the measured size. */ export function ShimmerText({ text, fontSize = 14, fontWeight = '500', }: { text: string; fontSize?: number; fontWeight?: TextStyle['fontWeight']; }) { // `useId` returns colon-wrapped ids (":r0:"); strip them so the value is a // safe SVG fragment identifier for `url(#…)` references in the web preview. const gradientId = `shimmer-${useId().replace(/:/g, '')}`; const [size, setSize] = useState({ width: 0, height: 0 }); const progress = useRef(new Animated.Value(0)).current; useEffect(() => { const animation = Animated.loop( Animated.timing(progress, { toValue: 1, duration: SHIMMER_DURATION_MS, easing: Easing.linear, useNativeDriver: false, }), ); animation.start(); return () => animation.stop(); }, [progress]); const width = size.width; const { x1, x2 } = useMemo(() => ({ x1: progress.interpolate({ inputRange: [0, 1], outputRange: [-width, width] }), x2: progress.interpolate({ inputRange: [0, 1], outputRange: [0, 2 * width] }), }), [progress, width]); return ( { const { width: w, height: h } = event.nativeEvent.layout; setSize((prev) => (prev.width === w && prev.height === h ? prev : { width: w, height: h })); }} style={{ fontSize, fontWeight, opacity: 0 }} > {text} {width > 0 ? ( {text} ) : null} ); }