Install

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

Dependencies

No required npm packages — this item installs through the registry dependencies below.

https://reactnativereusables.com/r/nativewind/button.json
https://reactnativereusables.com/r/nativewind/icon.json
https://reactnativereusables.com/r/nativewind/popover.json
https://reactnativereusables.com/r/nativewind/progress.json
https://reactnativereusables.com/r/nativewind/text.json
https://rnraielements.com/r/nativewind/mono.json

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

Source

context.tsx
import { Button } from '@/registry/nativewind/components/ui/button'; import { Icon } from '@/registry/nativewind/components/ui/icon'; import { Popover, PopoverContent, PopoverTrigger, } from '@/registry/nativewind/components/ui/popover'; import { Progress } from '@/registry/nativewind/components/ui/progress'; import { Text } from '@/registry/nativewind/components/ui/text'; import { monoStyle } from '@/registry/nativewind/lib/mono'; import { cn } from '@/registry/nativewind/lib/utils'; import { PieChartIcon } from 'lucide-react-native'; import * as React from 'react'; import { View, type ViewProps } from 'react-native'; import { contextUsedPercent, formatContextPercent, formatContextRatio, formatContextTokens, usageRowVisible, type ContextUsage, } from './context.logic'; /** * Context — the context-window budget chip (what the web original shows as a * hover-card trigger reading "62.5%" beside the composer). * * THE PRD VERDICT IS PORT-ADAPTED, PURE-RNR: "Token and cost breakdown is revealed on * hover in the web version. On touch it becomes a press-opened popover; the data * displayed is unchanged." The web composition maps 1:1 onto RNR's popover — * Context wraps Popover exactly as it wraps HoverCard, ContextTrigger wraps * PopoverTrigger, ContextContent renders PopoverContent: * * <Context usedTokens={…} maxTokens={…} usage={…}> * <ContextTrigger /> * <ContextContent insets={contentInsets}> * <ContextContentHeader /> * <ContextContentBody> * <ContextInputUsage /> <ContextOutputUsage /> * <ContextReasoningUsage /> <ContextCacheUsage /> * </ContextContentBody> * <ContextContentFooter costText="…?" /> * </ContextContent> * </Context> * * ContextContent takes the RNR popover content props — insets FIRST among them * (anchored content hides under the notch/nav bar without them; the consumer's * per-screen contentInsets ritual applies, as with every popover in the house). * * COST IS INJECTED, NOT COMPUTED. The web prices tokens with `tokenlens`, a model * pricing database this registry does not depend on. Every cost surface takes a * caller-formatted string instead (the renderMarkdown seam precedent): row * `costText` and footer `costText`. An unsupplied cost renders "—" — the web * original's own unknown-marker — never "$0.00", which would be a lie. * * THE RING GLYPH IS A DECLARED SUBSTITUTION. The web trigger draws an SVG * stroke-dashoffset progress ring; a raw react-native-svg element cannot receive * className without engine-specific cssInterop, which the styling contract forbids in * registry source, and an unthemeable stroke would fail the palette-flip proof. The * trigger ships the themed PieChartIcon instead — same at-a-glance "portion of whole" * semantics, tokens intact. The PRECISE bar is ContextContentHeader's RNR Progress, * which is theme-reachable. * * Two upstream behaviors preserved verbatim: the provider THROW * ("Context components must be used within Context"), and the zero-token rows * rendering nothing (usageRowVisible) — a breakdown never shows "Input 0". */ type ContextSchema = { usedTokens: number; maxTokens: number; usage?: ContextUsage; modelId?: string; }; const ContextContext = React.createContext<ContextSchema | null>(null); function useContextValue(): ContextSchema { const ctx = React.useContext(ContextContext); if (!ctx) throw new Error('Context components must be used within Context'); return ctx; } type ContextProps = ContextSchema & { children?: React.ReactNode; }; function Context({ usedTokens, maxTokens, usage, modelId, children }: ContextProps) { const contextValue = React.useMemo<ContextSchema>( () => ({ usedTokens, maxTokens, usage, modelId }), [usedTokens, maxTokens, usage, modelId], ); return ( <ContextContext.Provider value={contextValue}> <Popover>{children}</Popover> </ContextContext.Provider> ); } type ContextTriggerProps = { /** Overrides the default percent + gauge button (web: children on the trigger). */ children?: React.ReactNode; className?: string; }; function ContextTrigger({ children, className }: ContextTriggerProps) { const { usedTokens, maxTokens } = useContextValue(); return ( <PopoverTrigger asChild> {children ?? ( <Button variant="ghost" accessibilityLabel={`Model context usage, ${formatContextPercent(usedTokens, maxTokens)}`} className={cn('gap-1.5', className)} > <Text className="font-medium text-muted-foreground"> {formatContextPercent(usedTokens, maxTokens)} </Text> <Icon as={PieChartIcon} size={20} className="text-muted-foreground" /> </Button> )} </PopoverTrigger> ); } type ContextContentProps = React.ComponentProps<typeof PopoverContent> & { children?: React.ReactNode; }; /** * The press-opened breakdown. PopoverContent props flow through — insets, align, * sideOffset, portalHost — because the safe-area ritual belongs to the caller's * screen, not to this file. * * THE PORTAL RE-PROVIDE (the @rn-primitives/popover Portal precedent): the portal * transport stores this subtree's ELEMENTS in a zustand store and PortalHost — * mounted at the root layout, an ANCESTOR of the screen rendering <Context> — * renders them there, so React context resolves against the host's fiber and * ContextContext.Provider inside <Context> never reaches the portalled children * (the header/body/footer call useContextValue() and would throw * "Context components must be used within Context" on-device). So the value is * read OUTSIDE the portal — here, where composition still sits under Context's * provider — and re-provided INSIDE the boundary, exactly as the popover * primitive re-provides its own RootContext around the portalled children. */ function ContextContent({ className, children, ...props }: ContextContentProps) { const contextValue = useContextValue(); return ( <PopoverContent className={cn('w-60 gap-0 p-0', className)} {...props}> <ContextContext.Provider value={contextValue}>{children}</ContextContext.Provider> </PopoverContent> ); } type ContextContentHeaderProps = ViewProps & { children?: React.ReactNode }; /** Percentage, compact "used / total" in mono, and the progress bar. */ function ContextContentHeader({ className, children, ...props }: ContextContentHeaderProps) { const { usedTokens, maxTokens } = useContextValue(); const usedPercent = contextUsedPercent(usedTokens, maxTokens); return ( <View className={cn('w-full gap-2 border-b border-border p-3', className)} {...props}> {children ?? ( <> <View className="flex-row items-center justify-between gap-3"> <Text className="text-xs">{formatContextPercent(usedTokens, maxTokens)}</Text> <Text style={monoStyle} className="text-xs text-muted-foreground"> {formatContextRatio(usedTokens, maxTokens)} </Text> </View> <Progress value={usedPercent * 100} className="bg-muted" /> </> )} </View> ); } type ContextContentBodyProps = ViewProps & { children?: React.ReactNode }; function ContextContentBody({ className, children, ...props }: ContextContentBodyProps) { return ( <View className={cn('w-full gap-2 p-3', className)} {...props}> {children} </View> ); } type ContextContentFooterProps = ViewProps & { /** Caller-priced total (see header note — tokenlens is not a dependency). */ costText?: string; children?: React.ReactNode; }; /** * The total row on the secondary surface. The web original prints "$0.00" when it * cannot price; that is a lie on a port with no pricing data, so an unsupplied cost * renders the web original's own unknown-marker, "—". */ function ContextContentFooter({ costText, className, children, ...props }: ContextContentFooterProps) { return ( <View className={cn( 'flex-row w-full items-center justify-between gap-3 bg-secondary p-3', className, )} {...props} > {children ?? ( <> <Text className="text-xs text-muted-foreground">Total cost</Text> <Text className="text-xs">{costText ?? '—'}</Text> </> )} </View> ); } type ContextUsageRowProps = ViewProps & { /** Caller-priced cost for this slice — "• $0.0042" when supplied. */ costText?: string; children?: React.ReactNode; }; /** * One usage slice: label left, compact tokens (+ optional injected cost) right. * Children replace the token readout exactly as upstream allows; a zero or missing * count renders NOTHING (upstream trap parity). */ function ContextUsageRow({ label, tokens, costText, className, children, ...props }: ContextUsageRowProps & { label: string; tokens: number | undefined }) { if (children) { return ( <View className={cn('flex-row items-center justify-between', className)} {...props}> {children} </View> ); } if (!usageRowVisible(tokens)) return null; return ( <View className={cn('flex-row items-center justify-between', className)} {...props}> <Text className="text-xs text-muted-foreground">{label}</Text> <Text className="text-xs"> {formatContextTokens(tokens as number)} {costText ? ( <Text className="text-xs text-muted-foreground"> • {costText}</Text> ) : null} </Text> </View> ); } function ContextInputUsage(props: ContextUsageRowProps) { const { usage } = useContextValue(); return <ContextUsageRow label="Input" tokens={usage?.inputTokens} {...props} />; } function ContextOutputUsage(props: ContextUsageRowProps) { const { usage } = useContextValue(); return <ContextUsageRow label="Output" tokens={usage?.outputTokens} {...props} />; } function ContextReasoningUsage(props: ContextUsageRowProps) { const { usage } = useContextValue(); return <ContextUsageRow label="Reasoning" tokens={usage?.reasoningTokens} {...props} />; } function ContextCacheUsage(props: ContextUsageRowProps) { const { usage } = useContextValue(); return <ContextUsageRow label="Cache" tokens={usage?.cachedInputTokens} {...props} />; } export { Context, ContextCacheUsage, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextTrigger, }; export type { ContextUsage };
context.logic.ts
/** * Context — pure logic. Zero react-native imports, so the Vitest tier owns it (see the * header of sources.logic.ts for why component files themselves cannot load under * Node). * * The usage shape mirrors the AI SDK's LanguageModelUsage structurally, declared * locally per the ToolStatus precedent (the ONE kind of type allowed to mirror an * external contract, with no runtime dependency on the SDK). * * Cost does NOT compute here. The web original prices tokens with `tokenlens` — a model * pricing database we do not take as a dependency, so cost strings are injected by the * caller (the renderMarkdown seam precedent: an injected render over a hard binding). * Every formatter here is number-in/string-out and no-throw: a streamed usage object * with zero or missing fields is a rendering question, never a crash. */ /** Structurally identical to the AI SDK `LanguageModelUsage` fields the rows read. */ export type ContextUsage = { inputTokens?: number; outputTokens?: number; reasoningTokens?: number; cachedInputTokens?: number; }; /** * The consumed fraction of the window, clamped to [0, 1]. The web original divides * bare and would render NaN% on a zero max; a budget of zero means nothing used, not * infinity. */ export function contextUsedPercent(usedTokens: number, maxTokens: number): number { if (!maxTokens || maxTokens <= 0) return 0; const ratio = usedTokens / maxTokens; if (Number.isNaN(ratio)) return 0; return Math.min(1, Math.max(0, ratio)); } /** * The trigger and header percentage — the web original's exact formatter * (`Intl.NumberFormat('en-US', { maximumFractionDigits: 1, style: 'percent' })` over * the ratio), so 0.625 renders "62.5%" and 1 renders "100%". */ export function formatContextPercent(usedTokens: number, maxTokens: number): string { return new Intl.NumberFormat('en-US', { maximumFractionDigits: 1, style: 'percent', }).format(contextUsedPercent(usedTokens, maxTokens)); } /** * Compact token counts — the web original's exact formatter * (`Intl.NumberFormat('en-US', { notation: 'compact' })`), byte-parity with its * "12.3K / 200K" header readout. */ export function formatContextTokens(tokens: number): string { return new Intl.NumberFormat('en-US', { notation: 'compact' }).format(tokens); } /** * The usage-row visibility rule, upstream trap parity: a row whose token count is * missing or zero renders NOTHING — not a zero line. "Input — 0" is noise in a * breakdown the user opens to see where the window went. */ export function usageRowVisible(tokens: number | undefined): boolean { return Boolean(tokens); } /** * The composed "used / total" header readout, compact on both sides exactly as the * web original writes it (`{used} / {total}` with the spaces around the slash). */ export function formatContextRatio(usedTokens: number, maxTokens: number): string { return `${formatContextTokens(usedTokens)} / ${formatContextTokens(maxTokens)}`; }

Props

NameTypeDefaultDescription
childrenReact.ReactNode——

Theming

Themed via text-muted-foreground, border-border, bg-muted, bg-secondary.

Usage

() => ( <Context usedTokens={142_318} maxTokens={200_000} modelId={MODEL_ID}> <ContextTrigger /> </Context> )