react-native-ai-animations 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +2 -0
  2. package/lib/module/chat/effort-bar.js +38 -27
  3. package/lib/module/chat/effort-bar.js.map +1 -1
  4. package/lib/module/chat/question-card.js +1 -1
  5. package/lib/module/chat/question-card.js.map +1 -1
  6. package/lib/module/chat/status-row.js +6 -4
  7. package/lib/module/chat/status-row.js.map +1 -1
  8. package/lib/module/chat/thinking-block.js +3 -2
  9. package/lib/module/chat/thinking-block.js.map +1 -1
  10. package/lib/module/internal/color.js +24 -9
  11. package/lib/module/internal/color.js.map +1 -1
  12. package/lib/module/internal/graphemes.js +38 -0
  13. package/lib/module/internal/graphemes.js.map +1 -0
  14. package/lib/module/internal/use-elapsed.js +2 -1
  15. package/lib/module/internal/use-elapsed.js.map +1 -1
  16. package/lib/module/internal/use-motion-clock.js +11 -0
  17. package/lib/module/internal/use-motion-clock.js.map +1 -0
  18. package/lib/module/skia/dot-orb.js +5 -4
  19. package/lib/module/skia/dot-orb.js.map +1 -1
  20. package/lib/module/skia/matrix-orb.js +6 -3
  21. package/lib/module/skia/matrix-orb.js.map +1 -1
  22. package/lib/module/skia/media-generating.js +4 -3
  23. package/lib/module/skia/media-generating.js.map +1 -1
  24. package/lib/module/skia/voice-wave.js +8 -3
  25. package/lib/module/skia/voice-wave.js.map +1 -1
  26. package/lib/module/text/shimmer-text.js +22 -3
  27. package/lib/module/text/shimmer-text.js.map +1 -1
  28. package/lib/module/text/spoken-text.js +6 -1
  29. package/lib/module/text/spoken-text.js.map +1 -1
  30. package/lib/module/text/streaming-text.js +3 -3
  31. package/lib/module/text/streaming-text.js.map +1 -1
  32. package/lib/typescript/src/chat/effort-bar.d.ts.map +1 -1
  33. package/lib/typescript/src/chat/status-row.d.ts +2 -1
  34. package/lib/typescript/src/chat/status-row.d.ts.map +1 -1
  35. package/lib/typescript/src/chat/thinking-block.d.ts.map +1 -1
  36. package/lib/typescript/src/internal/color.d.ts +4 -0
  37. package/lib/typescript/src/internal/color.d.ts.map +1 -1
  38. package/lib/typescript/src/internal/graphemes.d.ts +4 -0
  39. package/lib/typescript/src/internal/graphemes.d.ts.map +1 -0
  40. package/lib/typescript/src/internal/use-elapsed.d.ts.map +1 -1
  41. package/lib/typescript/src/internal/use-motion-clock.d.ts +3 -0
  42. package/lib/typescript/src/internal/use-motion-clock.d.ts.map +1 -0
  43. package/lib/typescript/src/skia/dot-orb.d.ts.map +1 -1
  44. package/lib/typescript/src/skia/matrix-orb.d.ts +3 -1
  45. package/lib/typescript/src/skia/matrix-orb.d.ts.map +1 -1
  46. package/lib/typescript/src/skia/media-generating.d.ts.map +1 -1
  47. package/lib/typescript/src/skia/voice-wave.d.ts +2 -2
  48. package/lib/typescript/src/skia/voice-wave.d.ts.map +1 -1
  49. package/lib/typescript/src/text/shimmer-text.d.ts.map +1 -1
  50. package/lib/typescript/src/text/spoken-text.d.ts.map +1 -1
  51. package/lib/typescript/src/text/streaming-text.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/chat/effort-bar.tsx +40 -27
  54. package/src/chat/question-card.tsx +1 -1
  55. package/src/chat/status-row.tsx +7 -4
  56. package/src/chat/thinking-block.tsx +3 -2
  57. package/src/internal/color.ts +30 -15
  58. package/src/internal/graphemes.ts +40 -0
  59. package/src/internal/use-elapsed.ts +2 -1
  60. package/src/internal/use-motion-clock.ts +9 -0
  61. package/src/skia/dot-orb.tsx +5 -4
  62. package/src/skia/matrix-orb.tsx +7 -2
  63. package/src/skia/media-generating.tsx +3 -3
  64. package/src/skia/voice-wave.tsx +9 -4
  65. package/src/text/shimmer-text.tsx +25 -3
  66. package/src/text/spoken-text.tsx +5 -1
  67. package/src/text/streaming-text.tsx +6 -6
@@ -126,6 +126,7 @@ export function ThinkingBlock({
126
126
  };
127
127
 
128
128
  const heading = title ?? (thinking ? copy.thinking : copy.done(totalSeconds));
129
+ const label = thinking && startedAt !== undefined ? `${heading} ${elapsed}s` : heading;
129
130
 
130
131
  return (
131
132
  <View style={style}>
@@ -142,13 +143,13 @@ export function ThinkingBlock({
142
143
  <Animated.View key={thinking ? "thinking" : "done"} entering={reduceMotion ? undefined : FadeIn.duration(220)}>
143
144
  {thinking && !reduceMotion ? (
144
145
  <ShimmerText
145
- text={thinking && startedAt !== undefined ? `${heading} ${elapsed}s` : heading}
146
+ text={label}
146
147
  activeColor={palette.text}
147
148
  mutedColor={palette.textTertiary}
148
149
  textStyle={textStyle}
149
150
  />
150
151
  ) : (
151
- <Text style={[textStyle, { color: palette.textSecondary }]}>{heading}</Text>
152
+ <Text style={[textStyle, { color: palette.textSecondary }]}>{label}</Text>
152
153
  )}
153
154
  </Animated.View>
154
155
  {hasBody && (
@@ -1,18 +1,33 @@
1
+ import { processColor } from "react-native";
2
+
3
+ type Rgba = [number, number, number, number];
4
+
5
+ export function parseColor(color: string): Rgba | null {
6
+ const processed = processColor(color);
7
+ if (typeof processed !== "number") return null;
8
+ const argb = processed >>> 0;
9
+ return [(argb >>> 16) & 255, (argb >>> 8) & 255, argb & 255, (argb >>> 24) / 255];
10
+ }
11
+
12
+ function roundAlpha(value: number) {
13
+ return Math.round(value * 1000) / 1000;
14
+ }
15
+
1
16
  export function withAlpha(color: string, alpha: number): string {
2
- if (color.startsWith("rgb")) {
3
- const parts = color.replace(/rgba?\(|\)/g, "").split(",").map((part) => part.trim());
4
- return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${alpha})`;
17
+ const parsed = parseColor(color);
18
+ if (!parsed) return color;
19
+ const [red, green, blue, source] = parsed;
20
+ return `rgba(${red}, ${green}, ${blue}, ${roundAlpha(source * alpha)})`;
21
+ }
22
+
23
+ export function mixColors(from: string, to: string, t: number): string {
24
+ const a = parseColor(from);
25
+ const b = parseColor(to);
26
+ if (!a || !b) return t < 0.5 ? from : to;
27
+ const [red, green, blue, alpha] = a.map((value, index) => value + (b[index] - value) * t);
28
+ if (alpha >= 1) {
29
+ const hex = (value: number) => Math.round(value).toString(16).padStart(2, "0");
30
+ return `#${hex(red)}${hex(green)}${hex(blue)}`;
5
31
  }
6
- const hex = color.replace("#", "");
7
- const full =
8
- hex.length === 3
9
- ? hex
10
- .split("")
11
- .map((char) => char + char)
12
- .join("")
13
- : hex.slice(0, 6);
14
- const r = parseInt(full.slice(0, 2), 16);
15
- const g = parseInt(full.slice(2, 4), 16);
16
- const b = parseInt(full.slice(4, 6), 16);
17
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
32
+ return `rgba(${Math.round(red)}, ${Math.round(green)}, ${Math.round(blue)}, ${roundAlpha(alpha)})`;
18
33
  }
@@ -0,0 +1,40 @@
1
+ const EXTEND = /^[\p{M}\u200C\uFE00-\uFE0F\u{E0020}-\u{E007F}\u{1F3FB}-\u{1F3FF}]$/u;
2
+ const REGIONAL = /^[\u{1F1E6}-\u{1F1FF}]$/u;
3
+ const ZWJ = "\u200D";
4
+ const SHAPED = /[\u0590-\u08FF\u0900-\u0DFF\u0E00-\u0FFF\u1000-\u109F\u1780-\u18AF\uA800-\uABFF\uFB1D-\uFDFF\uFE70-\uFEFF]/;
5
+ const CJK = /^[\u2E80-\u303F\u3040-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF00-\uFFEF]/;
6
+
7
+ type Segmenter = { segment(text: string): Iterable<{ segment: string }> };
8
+ type SegmenterConstructor = new (locale?: string, options?: { granularity: "grapheme" }) => Segmenter;
9
+
10
+ function nativeSegmenter(): Segmenter | null {
11
+ const constructor = (Intl as unknown as { Segmenter?: SegmenterConstructor }).Segmenter;
12
+ return constructor ? new constructor(undefined, { granularity: "grapheme" }) : null;
13
+ }
14
+
15
+ export function splitGraphemes(text: string): string[] {
16
+ const segmenter = typeof Intl === "undefined" ? null : nativeSegmenter();
17
+ if (segmenter) return Array.from(segmenter.segment(text), (part) => part.segment);
18
+ const clusters: string[] = [];
19
+ let joinNext = false;
20
+ for (const point of text) {
21
+ const last = clusters.length - 1;
22
+ const previous = clusters[last];
23
+ const pairsFlag = REGIONAL.test(point) && previous !== undefined && REGIONAL.test(previous);
24
+ if (previous !== undefined && (joinNext || point === ZWJ || EXTEND.test(point) || pairsFlag)) {
25
+ clusters[last] = previous + point;
26
+ } else {
27
+ clusters.push(point);
28
+ }
29
+ joinNext = point === ZWJ;
30
+ }
31
+ return clusters;
32
+ }
33
+
34
+ export function needsShaping(text: string) {
35
+ return SHAPED.test(text);
36
+ }
37
+
38
+ export function isCjk(cluster: string) {
39
+ return CJK.test(cluster);
40
+ }
@@ -4,8 +4,9 @@ export function useElapsed(startedAt: number | undefined, running: boolean, inte
4
4
  const [now, setNow] = useState(() => Date.now());
5
5
 
6
6
  useEffect(() => {
7
- if (!running || startedAt === undefined) return;
7
+ if (startedAt === undefined) return;
8
8
  setNow(Date.now());
9
+ if (!running) return;
9
10
  const id = setInterval(() => setNow(Date.now()), interval);
10
11
  return () => clearInterval(id);
11
12
  }, [interval, running, startedAt]);
@@ -0,0 +1,9 @@
1
+ import { type SharedValue, useFrameCallback, useSharedValue } from "react-native-reanimated";
2
+
3
+ export function useMotionClock(running: boolean, initial = 0): SharedValue<number> {
4
+ const clock = useSharedValue(initial);
5
+ useFrameCallback((info) => {
6
+ clock.value += info.timeSincePreviousFrame ?? 0;
7
+ }, running);
8
+ return clock;
9
+ }
@@ -1,7 +1,8 @@
1
- import { Canvas, Path, Skia, type SkPath, useClock } from "@shopify/react-native-skia";
1
+ import { Canvas, Path, Skia, type SkPath } from "@shopify/react-native-skia";
2
2
  import { useEffect, useMemo } from "react";
3
3
  import { type StyleProp, type ViewStyle } from "react-native";
4
4
  import { Easing, useDerivedValue, useReducedMotion, useSharedValue, withDelay, withRepeat, withSequence, withTiming } from "react-native-reanimated";
5
+ import { useMotionClock } from "../internal/use-motion-clock";
5
6
  import { useMotionPalette } from "../theme/provider";
6
7
 
7
8
  export type DotOrbVariant = "orbit" | "scan" | "constellation" | "helix";
@@ -96,9 +97,9 @@ export function DotOrb({
96
97
  const reduceMotion = useReducedMotion();
97
98
  const ink = color ?? palette.text;
98
99
  const highlight = accent ?? palette.accent;
99
- const clock = useClock();
100
- const layout = useSharedValue({ width: size ?? 0, height: size ?? 0 });
101
100
  const still = reduceMotion || paused;
101
+ const clock = useMotionClock(!still);
102
+ const layout = useSharedValue({ width: size ?? 0, height: size ?? 0 });
102
103
  const spread = useSharedValue(scatter === true ? 1 : 0);
103
104
 
104
105
  useEffect(() => {
@@ -116,7 +117,7 @@ export function DotOrb({
116
117
  const edges = useMemo(() => (variant === "constellation" ? buildEdges(sphere, 0.6) : []), [sphere, variant]);
117
118
 
118
119
  const frame = useDerivedValue<Frame>(() => {
119
- const t = (still ? STILL_TIME : clock.value) * 0.001 * speed;
120
+ const t = (reduceMotion ? STILL_TIME : clock.value) * 0.001 * speed;
120
121
  const width = layout.value.width;
121
122
  const height = layout.value.height;
122
123
  const cx = width / 2;
@@ -7,6 +7,8 @@ import {
7
7
  useReducedMotion,
8
8
  useSharedValue,
9
9
  } from "react-native-reanimated";
10
+ import { type MotionPalette } from "../theme/palette";
11
+ import { useMotionPalette } from "../theme/provider";
10
12
 
11
13
  export type MatrixOrbState = "idle" | "listening" | "thinking";
12
14
 
@@ -24,7 +26,6 @@ const MAX_DELTA = 0.05;
24
26
  const MIN_DOTS = 3;
25
27
  const DEFAULT_DOTS = 11;
26
28
  const DEFAULT_SIZE = 240;
27
- const DEFAULT_COLOR = "#FFFFFF";
28
29
 
29
30
  const ORB_SPAN = 0.74;
30
31
  const ORB_DOT = 0.6;
@@ -195,6 +196,7 @@ export type MatrixOrbProps = {
195
196
  color?: string;
196
197
  stateColors?: Partial<Record<MatrixOrbState, string>>;
197
198
  dots?: number;
199
+ colors?: Partial<MotionPalette>;
198
200
  };
199
201
 
200
202
  export function MatrixOrb({
@@ -202,10 +204,13 @@ export function MatrixOrb({
202
204
  level,
203
205
  size = DEFAULT_SIZE,
204
206
  cover = false,
205
- color = DEFAULT_COLOR,
207
+ color: colorProp,
206
208
  stateColors,
207
209
  dots = DEFAULT_DOTS,
210
+ colors,
208
211
  }: MatrixOrbProps) {
212
+ const palette = useMotionPalette(colors);
213
+ const color = colorProp ?? palette.text;
209
214
  const reduceMotion = useReducedMotion();
210
215
 
211
216
  const grid = Math.max(MIN_DOTS, Math.round(dots));
@@ -7,7 +7,6 @@ import {
7
7
  LinearGradient,
8
8
  Paint,
9
9
  Rect,
10
- useClock,
11
10
  vec,
12
11
  } from "@shopify/react-native-skia";
13
12
  import { useEffect, useState, type ReactNode } from "react";
@@ -36,6 +35,7 @@ import Animated, {
36
35
  } from "react-native-reanimated";
37
36
  import { withAlpha } from "../internal/color";
38
37
  import { formatElapsed, useElapsed } from "../internal/use-elapsed";
38
+ import { useMotionClock } from "../internal/use-motion-clock";
39
39
  import { ShimmerText } from "../text/shimmer-text";
40
40
  import { moodstoneSwatches, type MotionPalette } from "../theme/palette";
41
41
  import { useMotionPalette } from "../theme/provider";
@@ -138,7 +138,7 @@ export function MediaGenerating({
138
138
  const seconds = useElapsed(startedAt, true);
139
139
  const copy = { ...DEFAULT_LABELS, ...labels };
140
140
  const title = status ? copy[status] : (label ?? copy.generating);
141
- const headline = chrome === "header" && label && status ? label : title;
141
+ const headline = chrome === "header" && label && status ? (status === "queued" ? `${label} \u00B7 ${copy.queued}` : label) : title;
142
142
  const percent = progress === undefined ? undefined : Math.round(Math.min(1, Math.max(0, progress)) * 100);
143
143
  const chip = withAlpha(palette.background, 0.72);
144
144
  const showSweep = sweep ?? variant === "blobs";
@@ -241,7 +241,7 @@ type BlobFieldProps = { colors: string[]; grain: boolean; sweep: boolean; sweepC
241
241
 
242
242
  function BlobField({ colors, grain, sweep, sweepColor, still }: BlobFieldProps) {
243
243
  const size = useSharedValue({ width: 0, height: 0 });
244
- const clock = useClock();
244
+ const clock = useMotionClock(!still);
245
245
  const time = useDerivedValue(() => (still ? 0 : clock.value), [still]);
246
246
  const canvasWidth = useDerivedValue(() => size.value.width);
247
247
  const canvasHeight = useDerivedValue(() => size.value.height);
@@ -1,7 +1,10 @@
1
- import { Canvas, LinearGradient, Path, Skia, useClock, vec } from "@shopify/react-native-skia";
1
+ import { Canvas, LinearGradient, Path, Skia, vec } from "@shopify/react-native-skia";
2
2
  import { useState } from "react";
3
3
  import { View, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
4
4
  import { type SharedValue, useDerivedValue, useFrameCallback, useReducedMotion, useSharedValue } from "react-native-reanimated";
5
+ import { useMotionClock } from "../internal/use-motion-clock";
6
+ import { moodstoneSwatches } from "../theme/palette";
7
+ import { useMotionPalette } from "../theme/provider";
5
8
 
6
9
  const DEFAULT_HEIGHT = 32;
7
10
  const SAMPLE_STEP = 2;
@@ -29,7 +32,7 @@ export type VoiceWaveVariant = "lines" | "bars" | "dots" | "pulse";
29
32
 
30
33
  export type VoiceWaveProps = {
31
34
  level: SharedValue<number>;
32
- colors: [string, string, string];
35
+ colors?: [string, string, string];
33
36
  variant?: VoiceWaveVariant;
34
37
  height?: number;
35
38
  style?: StyleProp<ViewStyle>;
@@ -168,10 +171,12 @@ function Pulse({ width, height, level, clock, colors }: FieldProps) {
168
171
  );
169
172
  }
170
173
 
171
- export function VoiceWave({ level, colors, variant = "lines", height = DEFAULT_HEIGHT, style }: VoiceWaveProps) {
174
+ export function VoiceWave({ level, colors: colorsProp, variant = "lines", height = DEFAULT_HEIGHT, style }: VoiceWaveProps) {
175
+ const palette = useMotionPalette();
176
+ const colors = colorsProp ?? [palette.accent, moodstoneSwatches.sky, moodstoneSwatches.mint];
172
177
  const [width, setWidth] = useState(0);
173
178
  const reduceMotion = useReducedMotion();
174
- const ticking = useClock();
179
+ const ticking = useMotionClock(!reduceMotion);
175
180
  const clock = useDerivedValue(() => (reduceMotion ? 1200 : ticking.value), [reduceMotion]);
176
181
 
177
182
  const handleLayout = (event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width);
@@ -14,6 +14,7 @@ import Animated, {
14
14
  withSequence,
15
15
  withTiming,
16
16
  } from "react-native-reanimated";
17
+ import { isCjk, needsShaping, splitGraphemes } from "../internal/graphemes";
17
18
 
18
19
  const DEFAULT_DURATION = 1400;
19
20
  const DEFAULT_HIGHLIGHT_WIDTH = 0.22;
@@ -50,20 +51,26 @@ export function ShimmerText({
50
51
  }: ShimmerTextProps) {
51
52
  const reduceMotion = useReducedMotion();
52
53
  const progress = useSharedValue(0);
54
+ const shaped = useMemo(() => needsShaping(text), [text]);
53
55
  const { words, total } = useMemo(() => {
54
- const characters = Array.from(text);
56
+ const characters = shaped ? [] : splitGraphemes(text);
55
57
  const groups: Glyph[][] = [];
56
58
  let current: Glyph[] = [];
57
59
  characters.forEach((char, index) => {
60
+ const breakAfter = char === " " || isCjk(char);
61
+ if (isCjk(char) && current.length) {
62
+ groups.push(current);
63
+ current = [];
64
+ }
58
65
  current.push({ char, index });
59
- if (char === " ") {
66
+ if (breakAfter) {
60
67
  groups.push(current);
61
68
  current = [];
62
69
  }
63
70
  });
64
71
  if (current.length) groups.push(current);
65
72
  return { words: groups, total: characters.length };
66
- }, [text]);
73
+ }, [shaped, text]);
67
74
 
68
75
  useEffect(() => {
69
76
  if (reduceMotion) return;
@@ -80,6 +87,21 @@ export function ShimmerText({
80
87
  return <Text style={[textStyle, { color: activeColor }]}>{text}</Text>;
81
88
  }
82
89
 
90
+ if (shaped) {
91
+ return (
92
+ <ShimmerChar
93
+ char={text}
94
+ index={1}
95
+ total={3}
96
+ progress={progress}
97
+ activeColor={activeColor}
98
+ mutedColor={mutedColor}
99
+ textStyle={textStyle}
100
+ highlightWidth={Math.max(highlightWidth, 0.5)}
101
+ />
102
+ );
103
+ }
104
+
83
105
  return (
84
106
  <View
85
107
  accessible
@@ -40,7 +40,11 @@ export function SpokenText({
40
40
  if (reduceMotion) {
41
41
  return (
42
42
  <Text accessibilityLabel={text} style={[textStyle, { color: active, textAlign: align }, style as StyleProp<TextStyle>]}>
43
- {text}
43
+ {words.map((word, index) => (
44
+ <Text key={index} style={{ color: index < current ? done : index === current ? active : upcoming }}>
45
+ {word}
46
+ </Text>
47
+ ))}
44
48
  </Text>
45
49
  );
46
50
  }
@@ -1,5 +1,5 @@
1
1
  import { isValidElement, memo, useEffect, useMemo, useState, type ReactNode } from "react";
2
- import { StyleSheet, Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
2
+ import { StyleSheet, Text, View, type ColorValue, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
3
3
  import Animated, {
4
4
  cancelAnimation,
5
5
  useAnimatedStyle,
@@ -9,7 +9,6 @@ import Animated, {
9
9
  withSequence,
10
10
  withTiming,
11
11
  } from "react-native-reanimated";
12
- import { withAlpha } from "../internal/color";
13
12
  import { type MotionPalette } from "../theme/palette";
14
13
  import { useMotionPalette } from "../theme/provider";
15
14
 
@@ -108,7 +107,8 @@ export function StreamingText({
108
107
  const [reveal, setReveal] = useState<Reveal>(() => (streaming && animate ? settledReveal("") : settledReveal(text)));
109
108
  const [now, setNow] = useState(() => Date.now());
110
109
  const fading = reveal.chunks.length > 0;
111
- const color = palette.text;
110
+ const flatStyle = StyleSheet.flatten(textStyle);
111
+ const color = flatStyle?.color ?? palette.text;
112
112
 
113
113
  useEffect(() => {
114
114
  if (!streaming || !animate) {
@@ -164,7 +164,7 @@ export function StreamingText({
164
164
  const value = text.slice(Math.max(piece.start, range.start), Math.min(piece.end, range.end));
165
165
  if (piece.alpha >= 1) return value;
166
166
  return (
167
- <Text key={piece.start} style={{ color: withAlpha(color, piece.alpha) }}>
167
+ <Text key={piece.start} style={{ opacity: piece.alpha }}>
168
168
  {value}
169
169
  </Text>
170
170
  );
@@ -180,7 +180,7 @@ export function StreamingText({
180
180
  type PlainParagraphProps = {
181
181
  value: string;
182
182
  textStyle: StyleProp<TextStyle>;
183
- color: string;
183
+ color: ColorValue;
184
184
  selectable: boolean;
185
185
  };
186
186
 
@@ -192,7 +192,7 @@ const PlainParagraph = memo(function PlainParagraph({ value, textStyle, color, s
192
192
  );
193
193
  });
194
194
 
195
- function Caret({ color, blink, textStyle }: { color: string; blink: boolean; textStyle: StyleProp<TextStyle> }) {
195
+ function Caret({ color, blink, textStyle }: { color: ColorValue; blink: boolean; textStyle: StyleProp<TextStyle> }) {
196
196
  const opacity = useSharedValue(1);
197
197
  const flat = StyleSheet.flatten(textStyle);
198
198
  const height = Math.round((flat?.fontSize ?? 16) * 1.05);