Install

npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/reasoning.json

Dependencies

https://reactnativereusables.com/r/nativewind/badge.json
https://reactnativereusables.com/r/nativewind/collapsible.json
https://reactnativereusables.com/r/nativewind/icon.json
https://reactnativereusables.com/r/nativewind/text.json
https://rnraielements.com/r/nativewind/reasoning-lifecycle.json
https://rnraielements.com/r/nativewind/markdown.json

AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92

Source

reasoning.tsx
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/nativewind/components/ui/collapsible'; import { Icon } from '@/registry/nativewind/components/ui/icon'; import { Text } from '@/registry/nativewind/components/ui/text'; import { repairIncompleteMarkdown } from '@/registry/nativewind/lib/markdown'; import { initReasoningLifecycle, reasoningLabel, reasoningLifecycleReducer, REASONING_AUTO_CLOSE_DELAY, shouldAutoClose, } from '@/registry/nativewind/lib/reasoning-lifecycle'; import { cn } from '@/registry/nativewind/lib/utils'; import { BrainIcon, ChevronDownIcon } from 'lucide-react-native'; import * as React from 'react'; import { View, type ViewProps } from 'react-native'; import Animated, { ReduceMotion, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; /** * Reasoning — the collapsible thinking trace. * * THE LIFECYCLE IS THE COMPONENT. It lives in lib/reasoning-lifecycle.ts as a pure, * time-injected reducer precisely so the Vitest tier can own it: auto-open when * `isStreaming` starts, auto-close 1000ms after it ends, `defaultOpen` pinned open and * never auto-closed, a mount with `isStreaming={false}` starting closed (loaded history * must not spring open), and — the rule that protects the reader — any user interaction * standing the automation down for good. This file only wires it: dispatch on * isStreaming transitions, arm a timer while the reducer says one should be armed. * * THE DURATION IS FROZEN, NOT TICKING. `durationSeconds` is computed once at stream end * and shown on the collapsed header ("Thought for 5 seconds"); while streaming the * header reads "Thinking…". A per-second live counter would chatter to screen readers — * the design contract bans it explicitly. A caller who already knows the duration can * pass it and the tracked value yields. * * MARKDOWN IS INJECTED, NOT BUNDLED — message.tsx's seam, for the same reason: the * native markdown renderer is a Fabric module that would make the whole component * dev-client-only. Default is plain RNR Text, with the streamed-half-token repair that * keeps `**bold` from flashing literal asterisks mid-stream. * * NO CONTROLLED `open` PROP, a deliberate divergence from the web original: the * lifecycle reducer is the component's value and a controlled open would fork the exact * logic the tests own. `onOpenChange` still reports every open change (including the * automatic ones) for consumers that want to mirror state; `defaultOpen` covers the * stay-open case. * * MOTION, RECORDED (remediation row 10 KEEP): the web's content enters/exits with * `animate-in/out` slide-fade utility classes (reasoning.tsx:214); RNR Collapsible's * native layout animation stands in for it — no animation code here on purpose. The * trigger label is the web's bare `text-sm text-muted-foreground` inline text * (reasoning.tsx:179), not a badge. */ type ReasoningContextValue = { streaming: boolean; open: boolean; /** Seconds, or null while streaming / before the first stream. */ durationSeconds: number | null; }; const ReasoningContext = React.createContext<ReasoningContextValue | null>(null); function useReasoning() { const ctx = React.useContext(ReasoningContext); if (!ctx) throw new Error('Reasoning components must be used within Reasoning'); return ctx; } type ReasoningProps = Omit<ViewProps, 'children'> & { isStreaming?: boolean; /** Pinned open. Never auto-closes, and suppresses the auto-open timer entirely. */ defaultOpen?: boolean; /** Overrides the tracked duration — for callers who already know it. */ duration?: number; onOpenChange?: (open: boolean) => void; children?: React.ReactNode; }; function Reasoning({ isStreaming = false, defaultOpen = false, duration, onOpenChange, className, children, ...props }: ReasoningProps) { const [lifecycle, dispatch] = React.useReducer( reasoningLifecycleReducer, defaultOpen, initReasoningLifecycle, ); // null = not yet observed, so mounting with isStreaming=true still opens the trace. const prevStreaming = React.useRef<boolean | null>(null); React.useEffect(() => { if (prevStreaming.current !== null && isStreaming === prevStreaming.current) return; if (isStreaming) dispatch({ type: 'stream-start', at: Date.now() }); else if (prevStreaming.current !== null) dispatch({ type: 'stream-end', at: Date.now() }); prevStreaming.current = isStreaming; }, [isStreaming]); React.useEffect(() => { if (!shouldAutoClose(lifecycle)) return; const timer = setTimeout(() => dispatch({ type: 'auto-close' }), REASONING_AUTO_CLOSE_DELAY); return () => clearTimeout(timer); }, [lifecycle]); const effectiveDuration = duration ?? lifecycle.durationSeconds; return ( <ReasoningContext.Provider value={{ streaming: isStreaming, open: lifecycle.open, durationSeconds: effectiveDuration }}> <Collapsible open={lifecycle.open} onOpenChange={(open) => { dispatch({ type: 'user-toggle', open }); onOpenChange?.(open); }} className={cn('w-full', className)} {...props} > {children} </Collapsible> </ReasoningContext.Provider> ); } /** House motion scale for the chevron: 250ms open, 200ms close, system-reduced. */ function Chevron({ open }: { open: boolean }) { const rotation = useSharedValue(open ? 1 : 0); React.useEffect(() => { rotation.value = withTiming(open ? 1 : 0, { duration: open ? 250 : 200, reduceMotion: ReduceMotion.System, }); }, [open, rotation]); const style = useAnimatedStyle(() => ({ transform: [{ rotate: `${rotation.value * 180}deg` }], })); return ( <Animated.View style={style}> <Icon as={ChevronDownIcon} size={14} className="text-muted-foreground" /> </Animated.View> ); } function ReasoningTrigger({ className }: { className?: string }) { const { streaming, open, durationSeconds } = useReasoning(); return ( <CollapsibleTrigger accessibilityState={{ busy: streaming, expanded: open }} className={cn( 'flex-row items-center gap-2 rounded-md py-1.5 active:bg-accent', className, )} > <Icon as={BrainIcon} size={14} className={cn(streaming ? 'text-foreground' : 'text-muted-foreground')} /> <Text accessibilityLiveRegion="polite" className="text-sm text-muted-foreground"> {reasoningLabel({ streaming, durationSeconds })} </Text> <View className="flex-1" /> <Chevron open={open} /> </CollapsibleTrigger> ); } export type RenderMarkdown = (markdown: string) => React.ReactNode; type ReasoningContentProps = { /** The reasoning trace, as a (possibly half-streamed) markdown string. */ children: string; /** Inject a markdown renderer. Omit it and the body renders as plain Text. */ renderMarkdown?: RenderMarkdown; /** Repair a half-streamed token mid-stream. On by default, as in message. */ parseIncompleteMarkdown?: boolean; className?: string; }; function ReasoningContent({ children, renderMarkdown, parseIncompleteMarkdown = true, className, }: ReasoningContentProps) { const text = React.useMemo( () => (parseIncompleteMarkdown ? repairIncompleteMarkdown(children) : children), [children, parseIncompleteMarkdown], ); return ( <CollapsibleContent className={cn('pb-1', className)}> {renderMarkdown ? ( renderMarkdown(text) ) : ( <Text className="text-sm leading-5 text-muted-foreground">{text}</Text> )} </CollapsibleContent> ); } export { Reasoning, ReasoningContent, ReasoningTrigger, useReasoning };

Props

NameTypeDefaultDescription
isStreamingboolean | undefinedfalse—
defaultOpenboolean | undefinedfalsePinned open. Never auto-closes, and suppresses the auto-open timer entirely.
durationnumber | undefined—Overrides the tracked duration — for callers who already know it.
onOpenChange((open: boolean) => void) | undefined——
childrenReact.ReactNode——

Theming

Themed via text-muted-foreground, bg-accent, text-foreground.

Usage

(o) => ( <Reasoning isStreaming={o.isStreaming === true} defaultOpen={o.defaultOpen === true} duration={4200} > <ReasoningContent> <Text className="text-sm text-muted-foreground"> The consumer installs from raw GitHub at the v0.2.0 tag, so the registry URLs must pin the release, not main. </Text> </ReasoningContent> </Reasoning> )