react-native-ai-animations 0.8.1 → 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 (68) hide show
  1. package/README.md +3 -1
  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 +151 -55
  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 +2 -2
  52. package/lib/typescript/src/text/streaming-text.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/chat/effort-bar.tsx +40 -27
  55. package/src/chat/question-card.tsx +1 -1
  56. package/src/chat/status-row.tsx +7 -4
  57. package/src/chat/thinking-block.tsx +3 -2
  58. package/src/internal/color.ts +30 -15
  59. package/src/internal/graphemes.ts +40 -0
  60. package/src/internal/use-elapsed.ts +2 -1
  61. package/src/internal/use-motion-clock.ts +9 -0
  62. package/src/skia/dot-orb.tsx +5 -4
  63. package/src/skia/matrix-orb.tsx +7 -2
  64. package/src/skia/media-generating.tsx +3 -3
  65. package/src/skia/voice-wave.tsx +9 -4
  66. package/src/text/shimmer-text.tsx +25 -3
  67. package/src/text/spoken-text.tsx +5 -1
  68. package/src/text/streaming-text.tsx +133 -57
@@ -1 +1 @@
1
- {"version":3,"file":"media-generating.d.ts","sourceRoot":"","sources":["../../../../src/skia/media-generating.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"media-generating.d.ts","sourceRoot":"","sources":["../../../../src/skia/media-generating.tsx"],"names":[],"mappings":"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"}
@@ -3,12 +3,12 @@ import { type SharedValue } from "react-native-reanimated";
3
3
  export type VoiceWaveVariant = "lines" | "bars" | "dots" | "pulse";
4
4
  export type VoiceWaveProps = {
5
5
  level: SharedValue<number>;
6
- colors: [string, string, string];
6
+ colors?: [string, string, string];
7
7
  variant?: VoiceWaveVariant;
8
8
  height?: number;
9
9
  style?: StyleProp<ViewStyle>;
10
10
  };
11
- export declare function VoiceWave({ level, colors, variant, height, style }: VoiceWaveProps): import("react").JSX.Element;
11
+ export declare function VoiceWave({ level, colors: colorsProp, variant, height, style }: VoiceWaveProps): import("react").JSX.Element;
12
12
  export type VoiceWaveStyleProps = Omit<VoiceWaveProps, "variant">;
13
13
  export declare function VoiceWaveLines(props: VoiceWaveStyleProps): import("react").JSX.Element;
14
14
  export declare function VoiceWaveBars(props: VoiceWaveStyleProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"voice-wave.d.ts","sourceRoot":"","sources":["../../../../src/skia/voice-wave.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAgC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5F,OAAO,EAAE,KAAK,WAAW,EAAuE,MAAM,yBAAyB,CAAC;AAwBhI,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEnE,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAuIF,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAiB,EAAE,MAAuB,EAAE,KAAK,EAAE,EAAE,cAAc,+BA0B7G;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;AAElE,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,+BAExD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,+BAEvD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,+BAEvD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,+BAExD"}
1
+ {"version":3,"file":"voice-wave.d.ts","sourceRoot":"","sources":["../../../../src/skia/voice-wave.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAgC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5F,OAAO,EAAE,KAAK,WAAW,EAAuE,MAAM,yBAAyB,CAAC;AA2BhI,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEnE,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAuIF,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAiB,EAAE,MAAuB,EAAE,KAAK,EAAE,EAAE,cAAc,+BA4BzH;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;AAElE,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,+BAExD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,+BAEvD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,+BAEvD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,+BAExD"}
@@ -1 +1 @@
1
- {"version":3,"file":"shimmer-text.d.ts","sourceRoot":"","sources":["../../../../src/text/shimmer-text.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA0B1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CAC3B,CAAC;AAIF,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,WAAW,EACX,UAAU,EACV,SAA8B,EAC9B,QAA2B,EAC3B,cAAwC,EACxC,KAAS,EACT,SAAiB,GAClB,EAAE,gBAAgB,+BA2DlB"}
1
+ {"version":3,"file":"shimmer-text.d.ts","sourceRoot":"","sources":["../../../../src/text/shimmer-text.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA2B1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CAC3B,CAAC;AAIF,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,WAAW,EACX,UAAU,EACV,SAA8B,EAC9B,QAA2B,EAC3B,cAAwC,EACxC,KAAS,EACT,SAAiB,GAClB,EAAE,gBAAgB,+BAgFlB"}
@@ -1 +1 @@
1
- {"version":3,"file":"spoken-text.d.ts","sourceRoot":"","sources":["../../../../src/text/spoken-text.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE1F,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAkB,CAAC;AAKtD,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,MAAM,EACN,SAA8B,EAC9B,WAAW,EACX,WAAW,EACX,aAAa,EACb,KAAgB,EAChB,MAAM,EACN,KAAK,GACN,EAAE,eAAe,+BAqCjB"}
1
+ {"version":3,"file":"spoken-text.d.ts","sourceRoot":"","sources":["../../../../src/text/spoken-text.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE1F,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAkB,CAAC;AAKtD,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,MAAM,EACN,SAA8B,EAC9B,WAAW,EACX,WAAW,EACX,aAAa,EACb,KAAgB,EAChB,MAAM,EACN,KAAK,GACN,EAAE,eAAe,+BAyCjB"}
@@ -7,10 +7,10 @@ export type StreamingTextProps = {
7
7
  textStyle?: StyleProp<TextStyle>;
8
8
  paragraphGap?: number;
9
9
  fadeDuration?: number;
10
- rise?: number;
11
10
  caret?: boolean | ReactNode;
11
+ selectable?: boolean;
12
12
  colors?: Partial<MotionPalette>;
13
13
  style?: StyleProp<ViewStyle>;
14
14
  };
15
- export declare function StreamingText({ text, streaming, textStyle, paragraphGap, fadeDuration, rise, caret, colors, style, }: StreamingTextProps): import("react").JSX.Element;
15
+ export declare function StreamingText({ text, streaming, textStyle, paragraphGap, fadeDuration, caret, selectable, colors, style, }: StreamingTextProps): import("react").JSX.Element;
16
16
  //# sourceMappingURL=streaming-text.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"streaming-text.d.ts","sourceRoot":"","sources":["../../../../src/text/streaming-text.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjF,OAAO,EAA0B,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAWtG,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAkB,CAAC;AAQtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AASF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,SAAS,EACT,SAA8B,EAC9B,YAAiB,EACjB,YAA2B,EAC3B,IAAmB,EACnB,KAAY,EACZ,MAAM,EACN,KAAK,GACN,EAAE,kBAAkB,+BA4CpB"}
1
+ {"version":3,"file":"streaming-text.d.ts","sourceRoot":"","sources":["../../../../src/text/streaming-text.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3F,OAAO,EAA2C,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAUvH,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAkB,CAAC;AAQtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AA+DF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,SAAS,EACT,SAA8B,EAC9B,YAAiB,EACjB,YAA2B,EAC3B,KAAY,EACZ,UAAkB,EAClB,MAAM,EACN,KAAK,GACN,EAAE,kBAAkB,+BA2EpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ai-animations",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Animated React Native and Expo components for AI chat apps: streaming text, shimmer loading text, thinking and status indicators, voice orbs and waveforms, image and video generation loaders, question cards and reasoning effort bars. Built on Reanimated and Skia.",
5
5
  "license": "MIT",
6
6
  "author": "Ahmet Kar",
@@ -20,7 +20,7 @@ import Animated, {
20
20
  useSharedValue,
21
21
  withSpring,
22
22
  } from "react-native-reanimated";
23
- import { withAlpha } from "../internal/color";
23
+ import { mixColors, withAlpha } from "../internal/color";
24
24
  import { type MotionPalette } from "../theme/palette";
25
25
  import { useMotionPalette } from "../theme/provider";
26
26
 
@@ -36,6 +36,7 @@ const SLIDER_TAB = 28;
36
36
  const SLIDER_TAB_RADIUS = 10;
37
37
  const SLIDER_SHOULDER = 8;
38
38
  const COMPACT_SLIDER_KNOB = 10;
39
+ const DRAG_SLOP = 4;
39
40
 
40
41
  export type EffortBarVariant = "bars" | "track" | "segmented" | "slider";
41
42
 
@@ -66,16 +67,6 @@ function normalize(level: EffortLevel): { key?: string; label: string; descripti
66
67
  return typeof level === "string" ? { label: level } : level;
67
68
  }
68
69
 
69
- function mix(a: string, b: string, t: number) {
70
- const parse = (hex: string) => {
71
- const clean = hex.replace("#", "");
72
- return [0, 2, 4].map((offset) => parseInt(clean.slice(offset, offset + 2), 16));
73
- };
74
- const from = parse(a);
75
- const to = parse(b);
76
- const channel = (index: number) => Math.round(from[index] + (to[index] - from[index]) * t).toString(16).padStart(2, "0");
77
- return `#${channel(0)}${channel(1)}${channel(2)}`;
78
- }
79
70
 
80
71
  function spread(stops: string[], count: number) {
81
72
  if (count <= 1) return [stops[stops.length - 1]];
@@ -86,7 +77,7 @@ function spread(stops: string[], count: number) {
86
77
  const high = Math.min(stops.length - 1, low + 1);
87
78
  const a = stops[low];
88
79
  const b = stops[high];
89
- return a.startsWith("#") && b.startsWith("#") ? mix(a, b, position - low) : position - low < 0.5 ? a : b;
80
+ return mixColors(a, b, position - low);
90
81
  });
91
82
  }
92
83
 
@@ -116,7 +107,7 @@ export function EffortBar({
116
107
  const reduceMotion = useReducedMotion();
117
108
  const items = useMemo(() => levels.map(normalize), [levels]);
118
109
  const count = Math.max(1, items.length);
119
- const index = Math.min(count - 1, Math.max(0, value));
110
+ const index = Math.min(count - 1, Math.max(0, Number.isFinite(value) ? Math.round(value) : 0));
120
111
  const current = items[index];
121
112
  const compact = size === "compact";
122
113
  const stops = fillColors && fillColors.length > 0 ? fillColors : undefined;
@@ -132,6 +123,8 @@ export function EffortBar({
132
123
  const lastIndex = useRef(index);
133
124
  const indexRef = useRef(index);
134
125
  const dragging = useRef(false);
126
+ const moved = useRef(false);
127
+ const startX = useRef(0);
135
128
  const interactive = !!onChange && !disabled;
136
129
  indexRef.current = index;
137
130
 
@@ -160,14 +153,15 @@ export function EffortBar({
160
153
  onChange?.(next, { index: next, label: item.label, value: item.value, budget: item.budget });
161
154
  };
162
155
 
163
- const pick = (x: number, grant: boolean) => {
156
+ const pick = (x: number, mode: "start" | "move" | "tap") => {
164
157
  if (!interactive || width === 0) return;
165
158
  let next: number;
166
159
  if (variant === "slider") {
167
160
  const position = Math.min(1, Math.max(0, (x - knobInset - knobSize / 2) / Math.max(1, width - knobInset * 2 - knobSize)));
168
- dragging.current = true;
169
- knob.value = reduceMotion || !grant ? position : withSpring(position, GRAB_SPRING);
170
- if (grant && !reduceMotion) pressed.value = withSpring(1, GRAB_SPRING);
161
+ if (mode !== "tap") {
162
+ dragging.current = true;
163
+ knob.value = reduceMotion || mode === "move" ? position : withSpring(position, GRAB_SPRING);
164
+ }
171
165
  next = Math.round(position * (count - 1));
172
166
  } else {
173
167
  next = Math.min(count - 1, Math.max(0, Math.floor((x / width) * count)));
@@ -176,15 +170,34 @@ export function EffortBar({
176
170
  lastIndex.current = next;
177
171
  select(next);
178
172
  };
179
- const pickRef = useRef(pick);
180
- pickRef.current = pick;
181
173
 
182
- const release = () => {
174
+ const grant = (x: number) => {
175
+ startX.current = x;
176
+ moved.current = false;
177
+ lastIndex.current = indexRef.current;
178
+ if (variant === "slider" && !reduceMotion) pressed.value = withSpring(1, GRAB_SPRING);
179
+ };
180
+ const grantRef = useRef(grant);
181
+ grantRef.current = grant;
182
+
183
+ const move = (x: number, dx: number) => {
184
+ if (!moved.current && Math.abs(dx) < DRAG_SLOP) return;
185
+ const first = !moved.current;
186
+ moved.current = true;
187
+ pick(x, first ? "start" : "move");
188
+ };
189
+ const moveRef = useRef(move);
190
+ moveRef.current = move;
191
+
192
+ const release = (commit: boolean) => {
193
+ const tap = commit && !moved.current;
194
+ moved.current = false;
195
+ if (tap) pick(startX.current, "tap");
196
+ if (variant === "slider") pressed.value = reduceMotion ? 0 : withSpring(0, LEVEL_SPRING);
183
197
  if (!dragging.current) return;
184
198
  dragging.current = false;
185
199
  const target = count > 1 ? indexRef.current / (count - 1) : 0;
186
200
  knob.value = reduceMotion ? target : withSpring(target, LEVEL_SPRING);
187
- pressed.value = reduceMotion ? 0 : withSpring(0, LEVEL_SPRING);
188
201
  };
189
202
  const releaseRef = useRef(release);
190
203
  releaseRef.current = release;
@@ -197,12 +210,12 @@ export function EffortBar({
197
210
  () =>
198
211
  PanResponder.create({
199
212
  onStartShouldSetPanResponder: () => true,
200
- onMoveShouldSetPanResponder: (_, gesture) => Math.abs(gesture.dx) > 4,
201
- onPanResponderTerminationRequest: () => false,
202
- onPanResponderGrant: (event) => pickRef.current(event.nativeEvent.locationX, true),
203
- onPanResponderMove: (event) => pickRef.current(event.nativeEvent.locationX, false),
204
- onPanResponderRelease: () => releaseRef.current(),
205
- onPanResponderTerminate: () => releaseRef.current(),
213
+ onMoveShouldSetPanResponder: (_, gesture) => Math.abs(gesture.dx) > DRAG_SLOP,
214
+ onPanResponderTerminationRequest: () => !moved.current,
215
+ onPanResponderGrant: (event) => grantRef.current(event.nativeEvent.locationX),
216
+ onPanResponderMove: (event, gesture) => moveRef.current(event.nativeEvent.locationX, gesture.dx),
217
+ onPanResponderRelease: () => releaseRef.current(true),
218
+ onPanResponderTerminate: () => releaseRef.current(false),
206
219
  }),
207
220
  [],
208
221
  );
@@ -333,7 +333,7 @@ export const QuestionCard = forwardRef<QuestionCardHandle, QuestionCardProps>(fu
333
333
  const padding: ViewStyle = sheet
334
334
  ? { paddingHorizontal: 20, paddingTop: 12, paddingBottom: 16 }
335
335
  : { paddingHorizontal: 16, paddingTop: 12, paddingBottom: 12 };
336
- const showFooter = kind === "multiple" && draft.selected.length > 0;
336
+ const showFooter = draft.selected.length > 0 && (kind === "multiple" || (last ? !autoSubmit : !autoAdvance));
337
337
 
338
338
  return (
339
339
  <Animated.View style={[container, shellStyle, style]}>
@@ -28,6 +28,7 @@ export type StatusRowProps = {
28
28
  iconSize?: number;
29
29
  active?: boolean;
30
30
  startedAt?: number;
31
+ seconds?: number;
31
32
  showTimer?: boolean;
32
33
  formatTimer?: (seconds: number) => string;
33
34
  textStyle?: StyleProp<TextStyle>;
@@ -43,6 +44,7 @@ export function StatusRow({
43
44
  iconSize = 18,
44
45
  active = true,
45
46
  startedAt,
47
+ seconds,
46
48
  showTimer = true,
47
49
  formatTimer = formatElapsed,
48
50
  textStyle = DEFAULT_TEXT_STYLE,
@@ -52,12 +54,13 @@ export function StatusRow({
52
54
  }: StatusRowProps) {
53
55
  const palette = useMotionPalette(colors);
54
56
  const reduceMotion = useReducedMotion();
55
- const seconds = useElapsed(startedAt, active);
57
+ const elapsed = useElapsed(startedAt, active);
58
+ const shownSeconds = seconds ?? elapsed;
56
59
 
57
60
  return (
58
61
  <View
59
62
  accessible
60
- accessibilityRole="progressbar"
63
+ accessibilityRole={active ? "progressbar" : "text"}
61
64
  accessibilityLabel={label}
62
65
  accessibilityState={{ busy: active }}
63
66
  style={[{ flexDirection: "row", alignItems: "center", gap: 8 }, style]}
@@ -83,9 +86,9 @@ export function StatusRow({
83
86
  )}
84
87
  </Animated.View>
85
88
  </View>
86
- {showTimer && startedAt !== undefined && (
89
+ {showTimer && (startedAt !== undefined || seconds !== undefined) && (
87
90
  <Text style={[textStyle, { color: palette.textTertiary, fontVariant: ["tabular-nums"] }, timerStyle]}>
88
- {formatTimer(seconds)}
91
+ {formatTimer(shownSeconds)}
89
92
  </Text>
90
93
  )}
91
94
  </View>
@@ -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
  }