{"slug":"code-block","name":"Code Block","description":"Streaming code with fixed line numbers and wrap.","group":"AI agents","detail_url":"https://beui.dev/native/r/code-block","page_url":"https://beui.dev/react-native/code-block","expo_go_url":"exp://u.expo.dev/93a3a95c-6dc2-4adb-b57c-efbecf859f25/--/code-block?runtime-version=exposdk%3A57.0.0&channel-name=production","source_url":"https://github.com/starc007/beui-native/tree/main/src/components/code-block","libraries":["prism-react-renderer","react","react-native","react-native-reanimated","react-native-svg","uniwind"],"install":["npx expo install prism-react-renderer react-native-reanimated react-native-svg react-native-worklets","npm install uniwind tailwindcss"],"setup":["Quickest: run `npx beui-native add <slug>` from the app's root. It writes the files below and installs the libraries.","Copy the files into the app and keep their folder layout: components import the shared files by relative path.","Add the theme tokens once, imported after Tailwind and Uniwind in the CSS file Uniwind reads.","Wrap the app in GestureHandlerRootView if it uses react-native-gesture-handler and isn't already.","Works in Expo and in bare React Native. Every component honors the system Reduce Motion setting."],"files":[{"path":"src/components/code-block/code-block.tsx","content":"import { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport {\n  Pressable,\n  ScrollView,\n  StyleSheet,\n  Text,\n  View,\n  type ViewProps,\n} from \"react-native\";\nimport Animated, {\n  useAnimatedStyle,\n  useSharedValue,\n  withSpring,\n} from \"react-native-reanimated\";\nimport { useResolveClassNames } from \"uniwind\";\nimport { useReducedMotionPreference } from \"../../hooks/use-reduced-motion\";\nimport {\n  CheckGlyph,\n  CopyGlyph,\n  FileGlyph,\n  WrapGlyph,\n} from \"../../primitives/agent-icons\";\nimport {\n  type CodeLanguage,\n  type CodeToken,\n  useCodeTokens,\n} from \"../../primitives/code-tokens\";\nimport { SpinGlyph } from \"../../primitives/spin-glyph\";\nimport { SPRING_PRESS } from \"../../styles/motion\";\n\nexport type CodeBlockStatus = \"streaming\" | \"complete\";\n\nconst LINE = 20;\nconst GUTTER = 44;\nconst COPIED_FOR = 1600;\n\n/** One line of tokens as nested Text, so colors stay on the line's baseline. */\nexport function CodeLineText({\n  tokens,\n  wrap,\n  className = \"\",\n}: {\n  tokens: CodeToken[];\n  wrap?: boolean;\n  className?: string;\n}) {\n  return (\n    <Text\n      numberOfLines={wrap ? undefined : 1}\n      className={`font-mono text-xs text-foreground/85 ${className}`}\n      style={styles.line}\n    >\n      {tokens.length === 0\n        ? \" \"\n        : tokens.map((token, index) => (\n            <Text\n              key={index}\n              style={token.color ? { color: token.color } : undefined}\n            >\n              {token.content}\n            </Text>\n          ))}\n    </Text>\n  );\n}\n\nfunction IconButton({\n  label,\n  selected,\n  reduce,\n  onPress,\n  children,\n}: {\n  label: string;\n  selected?: boolean;\n  reduce: boolean;\n  onPress: () => void;\n  children: ReactNode;\n}) {\n  const scale = useSharedValue(1);\n  const style = useAnimatedStyle(() => ({\n    transform: [{ scale: scale.value }],\n  }));\n  return (\n    <Pressable\n      accessibilityRole=\"button\"\n      accessibilityLabel={label}\n      accessibilityState={selected === undefined ? undefined : { selected }}\n      hitSlop={8}\n      onPress={onPress}\n      onPressIn={() => {\n        if (!reduce) scale.value = withSpring(0.9, SPRING_PRESS);\n      }}\n      onPressOut={() => {\n        scale.value = withSpring(1, SPRING_PRESS);\n      }}\n    >\n      <Animated.View style={[styles.iconButton, style]}>\n        {children}\n      </Animated.View>\n    </Pressable>\n  );\n}\n\nexport type CodeBlockProps = Omit<ViewProps, \"children\"> & {\n  code: string;\n  /** Defaults to \"typescript\". */\n  language?: CodeLanguage;\n  filename?: string;\n  /** Defaults to \"complete\". While streaming, the view follows new lines. */\n  status?: CodeBlockStatus;\n  /** Defaults to true. */\n  showLineNumbers?: boolean;\n  /** 1-based lines to tint. */\n  highlightLines?: number[];\n  /** Tallest the code grows before it scrolls. Defaults to 280. */\n  maxHeight?: number;\n  /** Starts wrapped. The header has a toggle either way. Defaults to false. */\n  wrap?: boolean;\n  /** Shows a copy button. Copy with your clipboard library here. */\n  onCopy?: () => unknown;\n  className?: string;\n  disableAnimation?: boolean;\n};\n\n/**\n * Highlighted code that streams in line by line. Line numbers stay put while\n * long lines scroll sideways, and a tap wraps them instead.\n */\nexport function CodeBlock({\n  code,\n  language = \"typescript\",\n  filename,\n  status = \"complete\",\n  showLineNumbers = true,\n  highlightLines = [],\n  maxHeight = 280,\n  wrap: initialWrap = false,\n  onCopy,\n  className = \"\",\n  disableAnimation = false,\n  ...props\n}: CodeBlockProps) {\n  const reducedMotion = useReducedMotionPreference();\n  const reduce = reducedMotion || disableAnimation;\n  const streaming = status === \"streaming\";\n  const lines = useCodeTokens(code, language);\n  const [wrap, setWrap] = useState(initialWrap);\n  const [copied, setCopied] = useState(false);\n  const [viewport, setViewport] = useState(0);\n  const copiedTimer = useRef<ReturnType<typeof setTimeout> | undefined>(\n    undefined,\n  );\n  const scrollRef = useRef<ScrollView>(null);\n  const following = useRef(true);\n  const dragging = useRef(false);\n  // The last lines often land with the switch to \"complete\", so the view\n  // follows one more size change after the stream ends.\n  const settling = useRef(false);\n  const wasStreaming = useRef(streaming);\n  if (wasStreaming.current !== streaming) {\n    settling.current = !streaming;\n    wasStreaming.current = streaming;\n  }\n  const highlighted = new Set(highlightLines);\n  const { color: muted } = useResolveClassNames(\"text-muted\");\n  const { color: foreground } = useResolveClassNames(\"text-foreground\");\n  const { color: primary } = useResolveClassNames(\"text-primary\");\n  const { color: success } = useResolveClassNames(\"text-success\");\n\n  useEffect(() => {\n    if (streaming) {\n      following.current = true;\n      return;\n    }\n    const timer = setTimeout(() => {\n      settling.current = false;\n    }, 500);\n    return () => clearTimeout(timer);\n  }, [streaming]);\n  useEffect(() => () => clearTimeout(copiedTimer.current), []);\n\n  const copy = async () => {\n    await onCopy?.();\n    setCopied(true);\n    clearTimeout(copiedTimer.current);\n    copiedTimer.current = setTimeout(() => setCopied(false), COPIED_FOR);\n  };\n\n  const tint = (index: number) =>\n    highlighted.has(index + 1) ? \"bg-primary/10\" : \"\";\n\n  const gutter = showLineNumbers ? (\n    <View style={{ width: GUTTER }}>\n      {lines.map((_, index) => (\n        <View key={index} style={styles.gutterRow} className={tint(index)}>\n          <Text\n            className=\"font-mono text-xs text-muted/50\"\n            style={[styles.line, styles.tabular]}\n          >\n            {index + 1}\n          </Text>\n        </View>\n      ))}\n    </View>\n  ) : null;\n\n  return (\n    <View\n      accessibilityState={{ busy: streaming }}\n      className={`w-full overflow-hidden rounded-2xl bg-surface ${className}`}\n      {...props}\n    >\n      <View style={styles.header} className=\"px-3\">\n        <FileGlyph size={14} color={muted} />\n        {filename ? (\n          <Text\n            numberOfLines={1}\n            className=\"shrink font-mono text-xs text-foreground/80\"\n          >\n            {filename}\n          </Text>\n        ) : null}\n        <Text\n          className=\"text-[10px] font-medium uppercase text-muted/70\"\n          style={styles.tracking}\n        >\n          {language}\n        </Text>\n        <View accessibilityLiveRegion=\"polite\" style={styles.status}>\n          {streaming ? (\n            <SpinGlyph size={12} color={primary} reduce={reduce} />\n          ) : (\n            <CheckGlyph size={12} color={success} />\n          )}\n          <Text\n            className={`text-[10px] font-medium ${streaming ? \"text-primary\" : \"text-success\"}`}\n          >\n            {streaming ? \"Writing\" : \"Ready\"}\n          </Text>\n        </View>\n        <IconButton\n          label=\"Wrap lines\"\n          selected={wrap}\n          reduce={reduce}\n          onPress={() => setWrap((value) => !value)}\n        >\n          <WrapGlyph size={14} color={wrap ? foreground : muted} />\n        </IconButton>\n        {onCopy ? (\n          <IconButton\n            label={copied ? \"Copied\" : \"Copy code\"}\n            reduce={reduce}\n            onPress={() => void copy()}\n          >\n            {copied ? (\n              <CheckGlyph size={14} color={foreground} />\n            ) : (\n              <CopyGlyph size={14} color={muted} />\n            )}\n          </IconButton>\n        ) : null}\n      </View>\n      {/* Padding sits outside the scroll area and its height snaps to whole\n          lines, so following the stream never leaves a half line at an edge. */}\n      <View className=\"border-t border-foreground/5 py-2\">\n        <ScrollView\n          ref={scrollRef}\n          style={{\n            maxHeight: Math.max(\n              LINE,\n              Math.floor((maxHeight - 16) / LINE) * LINE,\n            ),\n          }}\n          nestedScrollEnabled\n          showsVerticalScrollIndicator={false}\n          scrollEventThrottle={32}\n          onLayout={({ nativeEvent }) => setViewport(nativeEvent.layout.width)}\n          onScrollBeginDrag={() => {\n            dragging.current = true;\n          }}\n          onMomentumScrollEnd={() => {\n            dragging.current = false;\n          }}\n          onScrollEndDrag={({ nativeEvent }) => {\n            if (!nativeEvent.velocity?.y) dragging.current = false;\n          }}\n          onScroll={({ nativeEvent }) => {\n            if (!dragging.current) return;\n            const { contentOffset, contentSize, layoutMeasurement } =\n              nativeEvent;\n            following.current =\n              contentSize.height - contentOffset.y - layoutMeasurement.height <\n              24;\n          }}\n          onContentSizeChange={() => {\n            const follow = streaming || settling.current;\n            settling.current = false;\n            if (follow && following.current && !dragging.current) {\n              scrollRef.current?.scrollToEnd({ animated: !reduce });\n            }\n          }}\n        >\n          <View accessibilityLiveRegion={streaming ? \"polite\" : \"none\"}>\n            {wrap ? (\n              // Wrapped lines vary in height, so each row carries its own number.\n              lines.map((tokens, index) => (\n                <View key={index} style={styles.row} className={tint(index)}>\n                  {showLineNumbers ? (\n                    <Text\n                      className=\"font-mono text-xs text-muted/50\"\n                      style={[styles.line, styles.tabular, styles.number]}\n                    >\n                      {index + 1}\n                    </Text>\n                  ) : null}\n                  <CodeLineText\n                    tokens={tokens}\n                    wrap\n                    className={`flex-1 pr-4 ${showLineNumbers ? \"pl-1\" : \"pl-4\"}`}\n                  />\n                </View>\n              ))\n            ) : (\n              <View style={styles.row}>\n                {gutter}\n                {/* Only the code scrolls sideways; the numbers stay in place. */}\n                <ScrollView\n                  horizontal\n                  showsHorizontalScrollIndicator={false}\n                  style={styles.flex}\n                >\n                  <View\n                    style={{\n                      minWidth: Math.max(\n                        0,\n                        viewport - (showLineNumbers ? GUTTER : 0),\n                      ),\n                    }}\n                  >\n                    {lines.map((tokens, index) => (\n                      <View\n                        key={index}\n                        className={`${tint(index)} ${showLineNumbers ? \"pl-1\" : \"pl-4\"} pr-4`}\n                      >\n                        <CodeLineText tokens={tokens} />\n                      </View>\n                    ))}\n                  </View>\n                </ScrollView>\n              </View>\n            )}\n          </View>\n        </ScrollView>\n      </View>\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  flex: { flex: 1 },\n  header: { flexDirection: \"row\", alignItems: \"center\", gap: 10, height: 44 },\n  status: {\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    gap: 4,\n    marginLeft: \"auto\",\n  },\n  iconButton: {\n    width: 30,\n    height: 30,\n    borderRadius: 15,\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  },\n  line: { lineHeight: LINE, minHeight: LINE },\n  gutterRow: { height: LINE, alignItems: \"flex-end\", paddingRight: 12 },\n  number: { width: GUTTER, textAlign: \"right\", paddingRight: 12 },\n  row: { flexDirection: \"row\" },\n  tabular: { fontVariant: [\"tabular-nums\"] },\n  tracking: { letterSpacing: 0.4 },\n});\n","type":"component"},{"path":"src/components/code-block/index.ts","content":"export type { CodeLanguage } from \"../../primitives/code-tokens\";\nexport type { CodeBlockProps, CodeBlockStatus } from \"./code-block\";\nexport { CodeBlock, CodeLineText } from \"./code-block\";\n","type":"component"},{"path":"src/hooks/use-reduced-motion.ts","content":"import { useEffect, useState } from \"react\";\nimport { AccessibilityInfo } from \"react-native\";\nimport { useReducedMotion } from \"react-native-reanimated\";\n\n/** Reanimated supplies a synchronous initial value; native events keep it current. */\nexport function useReducedMotionPreference() {\n  const initialValue = useReducedMotion();\n  const [reducedMotion, setReducedMotion] = useState(initialValue);\n\n  useEffect(() => {\n    let active = true;\n    let receivedEvent = false;\n    const subscription = AccessibilityInfo.addEventListener(\n      \"reduceMotionChanged\",\n      (value) => {\n        receivedEvent = true;\n        setReducedMotion(value);\n      },\n    );\n    void AccessibilityInfo.isReduceMotionEnabled().then((value) => {\n      if (active && !receivedEvent) setReducedMotion(value);\n    });\n    return () => {\n      active = false;\n      subscription.remove();\n    };\n  }, []);\n\n  return reducedMotion;\n}\n","type":"shared"},{"path":"src/primitives/agent-icons.tsx","content":"import type { ReactNode } from \"react\";\nimport type { ColorValue } from \"react-native\";\nimport Svg, { Circle, Path, Rect } from \"react-native-svg\";\n\ntype GlyphProps = { size?: number; color?: ColorValue; strokeWidth?: number };\n\nfunction Glyph({\n  size = 16,\n  color = \"currentColor\",\n  strokeWidth = 2,\n  children,\n}: GlyphProps & { children: ReactNode }) {\n  return (\n    <Svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={strokeWidth}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      {children}\n    </Svg>\n  );\n}\n\n// Simple 24-unit stroke glyphs shared by the agent components.\n\nexport function ChevronDownGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M6 9l6 6 6-6\" />\n    </Glyph>\n  );\n}\n\nexport function ArrowUpGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M12 19V5M5.5 11.5L12 5l6.5 6.5\" />\n    </Glyph>\n  );\n}\n\nexport function PlusGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M12 5v14M5 12h14\" />\n    </Glyph>\n  );\n}\n\nexport function StopGlyph({ size = 12, color }: GlyphProps) {\n  return (\n    <Svg width={size} height={size} viewBox=\"0 0 12 12\">\n      <Rect x={1} y={1} width={10} height={10} rx={2.5} fill={color} />\n    </Svg>\n  );\n}\n\nexport function CheckGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M5 12.5l4.5 4.5L19 7\" />\n    </Glyph>\n  );\n}\n\nexport function CopyGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Rect x={8.5} y={8.5} width={12} height={12} rx={2.5} />\n      <Path d=\"M15.5 5.5v-.5A2 2 0 0 0 13.5 3h-8.5A2 2 0 0 0 3 5v8.5a2 2 0 0 0 2 2h.5\" />\n    </Glyph>\n  );\n}\n\nexport function RetryGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M3.5 12a8.5 8.5 0 1 0 2.6-6.1L3.5 8.5\" />\n      <Path d=\"M3.5 3.5v5h5\" />\n    </Glyph>\n  );\n}\n\nexport function ThumbGlyph({\n  down = false,\n  ...props\n}: GlyphProps & { down?: boolean }) {\n  return (\n    <Glyph {...props}>\n      <Path\n        d=\"M7.5 10.5v10M7.5 10.5L11 3.5a2.5 2.5 0 0 1 2.5 2.5v3.5h5a2 2 0 0 1 2 2.3l-1.2 7A2 2 0 0 1 17.3 20.5H4.5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1z\"\n        transform={down ? \"rotate(180 12 12)\" : undefined}\n      />\n    </Glyph>\n  );\n}\n\nexport function SearchGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Circle cx={11} cy={11} r={6.5} />\n      <Path d=\"M16 16l4 4\" />\n    </Glyph>\n  );\n}\n\nexport function GlobeGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Circle cx={12} cy={12} r={8.5} />\n      <Path d=\"M3.5 12h17M12 3.5c2.4 2.5 3.5 5.3 3.5 8.5s-1.1 6-3.5 8.5c-2.4-2.5-3.5-5.3-3.5-8.5s1.1-6 3.5-8.5z\" />\n    </Glyph>\n  );\n}\n\nexport function FileGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M14 3.5H7.5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V8z\" />\n      <Path d=\"M14 3.5V8h4.5M9 13h6M9 16.5h4\" />\n    </Glyph>\n  );\n}\n\nexport function PencilGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M4 20h4L19 9a2.1 2.1 0 0 0-4-4L4 16z\" />\n      <Path d=\"M13.5 6.5l4 4M14 20h6\" />\n    </Glyph>\n  );\n}\n\nexport function TerminalGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Rect x={3.5} y={4.5} width={17} height={15} rx={2.5} />\n      <Path d=\"M7.5 10l2.5 2-2.5 2M12.5 15h4\" />\n    </Glyph>\n  );\n}\n\nexport function WrenchGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M14.5 6.5a4 4 0 0 0 5 5l-8.8 8.8a2.1 2.1 0 0 1-3-3l8.8-8.8a4 4 0 0 1-2-2z\" />\n    </Glyph>\n  );\n}\n\nexport function SparkleGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M12 4l1.8 4.6L18.5 10.5l-4.7 1.9L12 17l-1.8-4.6-4.7-1.9 4.7-1.9z\" />\n      <Path d=\"M18.5 16.5v3M17 18h3\" />\n    </Glyph>\n  );\n}\n\nexport function MessageGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M20 14.5a2 2 0 0 1-2 2H8.5L4 20V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2z\" />\n    </Glyph>\n  );\n}\n\nexport function ImageGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Rect x={3.5} y={4.5} width={17} height={15} rx={2.5} />\n      <Circle cx={9} cy={10} r={1.5} />\n      <Path d=\"M20.5 15.5l-4.5-4.5-9 8.5\" />\n    </Glyph>\n  );\n}\n\nexport function ShieldGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M12 3.5l7 2.5v5.5c0 4.4-3 7.7-7 9-4-1.3-7-4.6-7-9V6z\" />\n      <Path d=\"M9 12l2 2 4-4\" />\n    </Glyph>\n  );\n}\n\nexport function AlertGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Circle cx={12} cy={12} r={8.5} />\n      <Path d=\"M12 8v4.5M12 16h.01\" />\n    </Glyph>\n  );\n}\n\nexport function CrossGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M6.5 6.5l11 11M17.5 6.5l-11 11\" />\n    </Glyph>\n  );\n}\n\nexport function HelpGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Circle cx={12} cy={12} r={8.5} />\n      <Path d=\"M9.6 9.5a2.5 2.5 0 0 1 4.8 1c0 1.7-2.4 2.2-2.4 3.5M12 16.8h.01\" />\n    </Glyph>\n  );\n}\n\nexport function ArrowGlyph({\n  back = false,\n  ...props\n}: GlyphProps & { back?: boolean }) {\n  return (\n    <Glyph {...props}>\n      <Path\n        d=\"M5 12h14M13 6l6 6-6 6\"\n        transform={back ? \"rotate(180 12 12)\" : undefined}\n      />\n    </Glyph>\n  );\n}\n\nexport function BracesGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M8.5 4H8a2 2 0 0 0-2 2v3.5A2.5 2.5 0 0 1 3.5 12 2.5 2.5 0 0 1 6 14.5V18a2 2 0 0 0 2 2h.5M15.5 4h.5a2 2 0 0 1 2 2v3.5a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0-2.5 2.5V18a2 2 0 0 1-2 2h-.5\" />\n    </Glyph>\n  );\n}\n\nexport function BanGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Circle cx={12} cy={12} r={8.5} />\n      <Path d=\"M6 6l12 12\" />\n    </Glyph>\n  );\n}\n\nexport function CircleCheckGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Circle cx={12} cy={12} r={8.5} />\n      <Path d=\"M8.5 12.2l2.4 2.4 4.6-4.8\" />\n    </Glyph>\n  );\n}\n\nexport function CircleCrossGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Circle cx={12} cy={12} r={8.5} />\n      <Path d=\"M9.5 9.5l5 5M14.5 9.5l-5 5\" />\n    </Glyph>\n  );\n}\n\nexport function ListGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M4 6.5l1.5 1.5L8 5.5M4 13l1.5 1.5L8 12M11.5 7h8.5M11.5 13.5h8.5M11.5 19h8.5M4.5 19h2\" />\n    </Glyph>\n  );\n}\n\n/** An open ring, spun by SpinGlyph. */\nexport function RingGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M20.5 12a8.5 8.5 0 1 1-6-8.1\" />\n    </Glyph>\n  );\n}\n\nexport function ReplyGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M9.5 7L4.5 12l5 5M4.5 12h10a5 5 0 0 1 5 5v1\" />\n    </Glyph>\n  );\n}\n\nexport function WrapGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M4 6.5h16M4 12h12.5a3.25 3.25 0 0 1 0 6.5H12M4 18.5h4.5M14 16.5l-2 2 2 2\" />\n    </Glyph>\n  );\n}\n\nexport function BookGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M12 6.5c-1.6-1.3-4-2-7.5-2v13c3.5 0 5.9.7 7.5 2 1.6-1.3 4-2 7.5-2v-13c-3.5 0-5.9.7-7.5 2zM12 6.5v13\" />\n    </Glyph>\n  );\n}\n\nexport function ExternalGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M14 4.5h5.5V10M19.5 4.5L11 13M17 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h4\" />\n    </Glyph>\n  );\n}\n\nexport function DoubleCheckGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Path d=\"M2.5 12.5l4.5 4.5 9-9.5M12 16.5l.5.5 9-9.5\" />\n    </Glyph>\n  );\n}\n\nexport function ClockGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Circle cx={12} cy={12} r={8.5} />\n      <Path d=\"M12 7.5V12l3 2\" />\n    </Glyph>\n  );\n}\n\nexport function MicGlyph(props: GlyphProps) {\n  return (\n    <Glyph {...props}>\n      <Rect x={9} y={3.5} width={6} height={11} rx={3} />\n      <Path d=\"M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5v3\" />\n    </Glyph>\n  );\n}\n","type":"shared"},{"path":"src/primitives/code-tokens.ts","content":"import {\n  normalizeTokens,\n  Prism,\n  type PrismTheme,\n  themes,\n} from \"prism-react-renderer\";\nimport { useMemo } from \"react\";\nimport { useUniwind } from \"uniwind\";\n\nexport type CodeLanguage =\n  | \"bash\"\n  | \"css\"\n  | \"go\"\n  | \"javascript\"\n  | \"json\"\n  | \"jsx\"\n  | \"kotlin\"\n  | \"markdown\"\n  | \"python\"\n  | \"rust\"\n  | \"sql\"\n  | \"swift\"\n  | \"text\"\n  | \"tsx\"\n  | \"typescript\"\n  | \"yaml\";\n\nexport type CodeToken = { content: string; color?: string };\nexport type CodeTokenLines = CodeToken[][];\n\n// Prism's bundle has no shell grammar; this covers what agents usually run.\nif (!Prism.languages.bash) {\n  Prism.languages.bash = {\n    comment: { pattern: /(^|\\s)#.*/, lookbehind: true },\n    string: { pattern: /([\"'])(?:\\\\.|(?!\\1)[^\\\\])*\\1/, greedy: true },\n    variable: /\\$(?:\\w+|\\{[^}]+\\})/,\n    function: { pattern: /(^|[|;&]\\s*|\\$\\s+)[\\w./-]+/m, lookbehind: true },\n    keyword:\n      /\\b(?:if|then|else|elif|fi|for|while|do|done|case|esac|in|function|export|return)\\b/,\n    property: /(^|\\s)--?[\\w-]+/,\n    operator: /&&|\\|\\||[|;&<>]/,\n    number: /\\b\\d+(?:\\.\\d+)?\\b/,\n  };\n}\n\nconst LIGHT = themes.github;\nconst DARK = themes.oneDark;\n\nfunction palette(theme: PrismTheme) {\n  const colors = new Map<string, string>();\n  for (const style of theme.styles) {\n    const color = style.style.color;\n    if (!color) continue;\n    for (const type of style.types) colors.set(type, color);\n  }\n  return colors;\n}\nconst PALETTES = { light: palette(LIGHT), dark: palette(DARK) };\n\n// Small, bounded cache: streaming code re-tokenizes a growing string.\nconst cache = new Map<string, string[][][]>();\nconst CACHE_SIZE = 64;\n\n/** Splits code into lines of tokens, each token tagged with its Prism types. */\nfunction tokenize(code: string, language: CodeLanguage) {\n  const key = `${language}\\u0000${code}`;\n  const hit = cache.get(key);\n  if (hit) return hit;\n  const grammar = language === \"text\" ? undefined : Prism.languages[language];\n  const lines = grammar\n    ? normalizeTokens(Prism.tokenize(code, grammar)).map((line) =>\n        line.map((token) => [token.content, ...token.types]),\n      )\n    : code.split(\"\\n\").map((line) => [[line, \"plain\"]]);\n  cache.set(key, lines);\n  if (cache.size > CACHE_SIZE)\n    cache.delete(cache.keys().next().value as string);\n  return lines;\n}\n\n/**\n * Syntax-highlighted lines for the current light or dark theme. Tokenizing is\n * synchronous, so streamed code never flashes back to plain text.\n */\nexport function useCodeTokens(\n  code: string,\n  language: CodeLanguage,\n): CodeTokenLines {\n  const { theme } = useUniwind();\n  const colors = theme === \"dark\" ? PALETTES.dark : PALETTES.light;\n  return useMemo(\n    () =>\n      tokenize(code, language).map((line) =>\n        line.map(([content = \"\", ...types]) => {\n          let color: string | undefined;\n          for (const type of types) color = colors.get(type) ?? color;\n          return { content, color };\n        }),\n      ),\n    [code, colors, language],\n  );\n}\n","type":"shared"},{"path":"src/primitives/spin-glyph.tsx","content":"import { useEffect } from \"react\";\nimport type { ColorValue } from \"react-native\";\nimport Animated, {\n  cancelAnimation,\n  Easing,\n  ReduceMotion,\n  useAnimatedStyle,\n  useSharedValue,\n  withRepeat,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { RingGlyph } from \"./agent-icons\";\n\n/**\n * A spinning ring for busy states. Reduced motion holds it still; the ring\n * still reads as \"in progress\".\n */\nexport function SpinGlyph({\n  size = 16,\n  color,\n  reduce,\n}: {\n  size?: number;\n  color?: ColorValue;\n  reduce: boolean;\n}) {\n  const turn = useSharedValue(0);\n\n  useEffect(() => {\n    if (reduce) {\n      cancelAnimation(turn);\n      turn.value = 0;\n      return;\n    }\n    turn.value = 0;\n    turn.value = withRepeat(\n      withTiming(1, {\n        duration: 900,\n        easing: Easing.linear,\n        reduceMotion: ReduceMotion.Never,\n      }),\n      -1,\n      false,\n    );\n    return () => cancelAnimation(turn);\n  }, [reduce, turn]);\n\n  const style = useAnimatedStyle(() => ({\n    transform: [{ rotate: `${turn.value * 360}deg` }],\n  }));\n\n  return (\n    <Animated.View style={style}>\n      <RingGlyph size={size} color={color} />\n    </Animated.View>\n  );\n}\n","type":"shared"},{"path":"src/styles/motion.ts","content":"import { Easing, ReduceMotion } from \"react-native-reanimated\";\n\n// Mirrors beUI's lib/ease.ts so native motion matches the web library.\n\nexport const EASE_OUT = Easing.bezier(0.16, 1, 0.3, 1);\nexport const EASE_IN_OUT = Easing.bezier(0.77, 0, 0.175, 1);\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n  reduceMotion: ReduceMotion.System,\n} as const;\n\n/** Content swaps: label and icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n  reduceMotion: ReduceMotion.System,\n} as const;\n\n/** Shared-layout glides: pills, indicators and panels moving between positions. */\nexport const SPRING_LAYOUT = {\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n  reduceMotion: ReduceMotion.System,\n} as const;\n\n/** Dragged handles and fills; critically damped so values never rebound off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n  reduceMotion: ReduceMotion.System,\n} as const;\n\nexport const motion = {\n  press: SPRING_PRESS,\n  swap: SPRING_SWAP,\n  layout: SPRING_LAYOUT,\n  glide: SPRING_GLIDE,\n  exit: { duration: 160, easing: EASE_OUT, reduceMotion: ReduceMotion.System },\n  fade: { duration: 150, reduceMotion: ReduceMotion.System },\n} as const;\n","type":"shared"},{"path":"src/styles/theme.css","content":"/* Matches the default beUI palette in ui-components/lib/themes.ts.\n   Native surface maps to card; muted maps to muted-foreground. */\n@layer theme {\n  :root {\n    @variant light {\n      --color-background: oklch(99% 0 0);\n      --color-surface: oklch(97% 0 0);\n      --color-foreground: oklch(15% 0 0);\n      --color-muted: oklch(50% 0 0);\n      --color-border: oklch(15% 0 0 / 0.06);\n      --color-border-strong: oklch(15% 0 0 / 0.16);\n      --color-primary: #0285f7;\n      --color-on-primary: #ffffff;\n      --color-destructive: #dc2626;\n      --color-success: #059669;\n      --color-warning: #d97706;\n    }\n  }\n\n  :root {\n    @variant dark {\n      --color-background: #151515;\n      --color-surface: #1c1c1c;\n      --color-foreground: oklch(96% 0 0);\n      --color-muted: oklch(62% 0 0);\n      --color-border: rgb(255 255 255 / 0.05);\n      --color-border-strong: rgb(255 255 255 / 0.14);\n      --color-primary: #0285f7;\n      --color-on-primary: #ffffff;\n      --color-destructive: #f87171;\n      --color-success: #34d399;\n      --color-warning: #fbbf24;\n    }\n  }\n\n  /* Tailwind's default monospace stack isn't a font React Native can load. */\n  :root {\n    @variant ios {\n      --font-mono: Menlo;\n    }\n  }\n\n  :root {\n    @variant android {\n      --font-mono: monospace;\n    }\n  }\n}\n","type":"theme"}]}