react-native-laminar 1.0.0

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 (168) hide show
  1. package/README.md +11 -0
  2. package/lib/commonjs/hooks/use-display-units.js +13 -0
  3. package/lib/commonjs/hooks/use-display-units.js.map +1 -0
  4. package/lib/commonjs/hooks/use-inline-auto-width.js +45 -0
  5. package/lib/commonjs/hooks/use-inline-auto-width.js.map +1 -0
  6. package/lib/commonjs/hooks/use-morph-motion.js +24 -0
  7. package/lib/commonjs/hooks/use-morph-motion.js.map +1 -0
  8. package/lib/commonjs/hooks/use-morph-text-style.js +32 -0
  9. package/lib/commonjs/hooks/use-morph-text-style.js.map +1 -0
  10. package/lib/commonjs/hooks/use-numeric-lanes.js +37 -0
  11. package/lib/commonjs/hooks/use-numeric-lanes.js.map +1 -0
  12. package/lib/commonjs/hooks/use-text-glyphs.js +35 -0
  13. package/lib/commonjs/hooks/use-text-glyphs.js.map +1 -0
  14. package/lib/commonjs/index.js +89 -0
  15. package/lib/commonjs/index.js.map +1 -0
  16. package/lib/commonjs/model/display-units.js +29 -0
  17. package/lib/commonjs/model/display-units.js.map +1 -0
  18. package/lib/commonjs/model/numeric-lanes.js +45 -0
  19. package/lib/commonjs/model/numeric-lanes.js.map +1 -0
  20. package/lib/commonjs/model/text-keys.js +80 -0
  21. package/lib/commonjs/model/text-keys.js.map +1 -0
  22. package/lib/commonjs/motion/entry-exit-builders.js +57 -0
  23. package/lib/commonjs/motion/entry-exit-builders.js.map +1 -0
  24. package/lib/commonjs/motion/preset-map.js +91 -0
  25. package/lib/commonjs/motion/preset-map.js.map +1 -0
  26. package/lib/commonjs/package.json +1 -0
  27. package/lib/commonjs/types.js +6 -0
  28. package/lib/commonjs/types.js.map +1 -0
  29. package/lib/commonjs/view/glyph-run.js +34 -0
  30. package/lib/commonjs/view/glyph-run.js.map +1 -0
  31. package/lib/commonjs/view/morph-viewport.js +64 -0
  32. package/lib/commonjs/view/morph-viewport.js.map +1 -0
  33. package/lib/commonjs/view/number-lane.js +85 -0
  34. package/lib/commonjs/view/number-lane.js.map +1 -0
  35. package/lib/commonjs/view/number-run.js +61 -0
  36. package/lib/commonjs/view/number-run.js.map +1 -0
  37. package/lib/commonjs/view/text-run.js +35 -0
  38. package/lib/commonjs/view/text-run.js.map +1 -0
  39. package/lib/module/hooks/use-display-units.js +8 -0
  40. package/lib/module/hooks/use-display-units.js.map +1 -0
  41. package/lib/module/hooks/use-inline-auto-width.js +40 -0
  42. package/lib/module/hooks/use-inline-auto-width.js.map +1 -0
  43. package/lib/module/hooks/use-morph-motion.js +19 -0
  44. package/lib/module/hooks/use-morph-motion.js.map +1 -0
  45. package/lib/module/hooks/use-morph-text-style.js +27 -0
  46. package/lib/module/hooks/use-morph-text-style.js.map +1 -0
  47. package/lib/module/hooks/use-numeric-lanes.js +32 -0
  48. package/lib/module/hooks/use-numeric-lanes.js.map +1 -0
  49. package/lib/module/hooks/use-text-glyphs.js +30 -0
  50. package/lib/module/hooks/use-text-glyphs.js.map +1 -0
  51. package/lib/module/index.js +84 -0
  52. package/lib/module/index.js.map +1 -0
  53. package/lib/module/model/display-units.js +21 -0
  54. package/lib/module/model/display-units.js.map +1 -0
  55. package/lib/module/model/numeric-lanes.js +40 -0
  56. package/lib/module/model/numeric-lanes.js.map +1 -0
  57. package/lib/module/model/text-keys.js +75 -0
  58. package/lib/module/model/text-keys.js.map +1 -0
  59. package/lib/module/motion/entry-exit-builders.js +52 -0
  60. package/lib/module/motion/entry-exit-builders.js.map +1 -0
  61. package/lib/module/motion/preset-map.js +86 -0
  62. package/lib/module/motion/preset-map.js.map +1 -0
  63. package/lib/module/package.json +1 -0
  64. package/lib/module/types.js +4 -0
  65. package/lib/module/types.js.map +1 -0
  66. package/lib/module/view/glyph-run.js +29 -0
  67. package/lib/module/view/glyph-run.js.map +1 -0
  68. package/lib/module/view/morph-viewport.js +58 -0
  69. package/lib/module/view/morph-viewport.js.map +1 -0
  70. package/lib/module/view/number-lane.js +79 -0
  71. package/lib/module/view/number-lane.js.map +1 -0
  72. package/lib/module/view/number-run.js +56 -0
  73. package/lib/module/view/number-run.js.map +1 -0
  74. package/lib/module/view/text-run.js +30 -0
  75. package/lib/module/view/text-run.js.map +1 -0
  76. package/lib/typescript/commonjs/hooks/use-display-units.d.ts +2 -0
  77. package/lib/typescript/commonjs/hooks/use-display-units.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts +15 -0
  79. package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts +14 -0
  81. package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/hooks/use-morph-text-style.d.ts +13 -0
  83. package/lib/typescript/commonjs/hooks/use-morph-text-style.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/hooks/use-numeric-lanes.d.ts +10 -0
  85. package/lib/typescript/commonjs/hooks/use-numeric-lanes.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/hooks/use-text-glyphs.d.ts +3 -0
  87. package/lib/typescript/commonjs/hooks/use-text-glyphs.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/index.d.ts +7 -0
  89. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/model/display-units.d.ts +5 -0
  91. package/lib/typescript/commonjs/model/display-units.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/model/numeric-lanes.d.ts +9 -0
  93. package/lib/typescript/commonjs/model/numeric-lanes.d.ts.map +1 -0
  94. package/lib/typescript/commonjs/model/text-keys.d.ts +7 -0
  95. package/lib/typescript/commonjs/model/text-keys.d.ts.map +1 -0
  96. package/lib/typescript/commonjs/motion/entry-exit-builders.d.ts +15 -0
  97. package/lib/typescript/commonjs/motion/entry-exit-builders.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/motion/preset-map.d.ts +4 -0
  99. package/lib/typescript/commonjs/motion/preset-map.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/package.json +1 -0
  101. package/lib/typescript/commonjs/types.d.ts +43 -0
  102. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/view/glyph-run.d.ts +12 -0
  104. package/lib/typescript/commonjs/view/glyph-run.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/view/morph-viewport.d.ts +14 -0
  106. package/lib/typescript/commonjs/view/morph-viewport.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/view/number-lane.d.ts +17 -0
  108. package/lib/typescript/commonjs/view/number-lane.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/view/number-run.d.ts +13 -0
  110. package/lib/typescript/commonjs/view/number-run.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/view/text-run.d.ts +11 -0
  112. package/lib/typescript/commonjs/view/text-run.d.ts.map +1 -0
  113. package/lib/typescript/module/hooks/use-display-units.d.ts +2 -0
  114. package/lib/typescript/module/hooks/use-display-units.d.ts.map +1 -0
  115. package/lib/typescript/module/hooks/use-inline-auto-width.d.ts +15 -0
  116. package/lib/typescript/module/hooks/use-inline-auto-width.d.ts.map +1 -0
  117. package/lib/typescript/module/hooks/use-morph-motion.d.ts +14 -0
  118. package/lib/typescript/module/hooks/use-morph-motion.d.ts.map +1 -0
  119. package/lib/typescript/module/hooks/use-morph-text-style.d.ts +13 -0
  120. package/lib/typescript/module/hooks/use-morph-text-style.d.ts.map +1 -0
  121. package/lib/typescript/module/hooks/use-numeric-lanes.d.ts +10 -0
  122. package/lib/typescript/module/hooks/use-numeric-lanes.d.ts.map +1 -0
  123. package/lib/typescript/module/hooks/use-text-glyphs.d.ts +3 -0
  124. package/lib/typescript/module/hooks/use-text-glyphs.d.ts.map +1 -0
  125. package/lib/typescript/module/index.d.ts +7 -0
  126. package/lib/typescript/module/index.d.ts.map +1 -0
  127. package/lib/typescript/module/model/display-units.d.ts +5 -0
  128. package/lib/typescript/module/model/display-units.d.ts.map +1 -0
  129. package/lib/typescript/module/model/numeric-lanes.d.ts +9 -0
  130. package/lib/typescript/module/model/numeric-lanes.d.ts.map +1 -0
  131. package/lib/typescript/module/model/text-keys.d.ts +7 -0
  132. package/lib/typescript/module/model/text-keys.d.ts.map +1 -0
  133. package/lib/typescript/module/motion/entry-exit-builders.d.ts +15 -0
  134. package/lib/typescript/module/motion/entry-exit-builders.d.ts.map +1 -0
  135. package/lib/typescript/module/motion/preset-map.d.ts +4 -0
  136. package/lib/typescript/module/motion/preset-map.d.ts.map +1 -0
  137. package/lib/typescript/module/package.json +1 -0
  138. package/lib/typescript/module/types.d.ts +43 -0
  139. package/lib/typescript/module/types.d.ts.map +1 -0
  140. package/lib/typescript/module/view/glyph-run.d.ts +12 -0
  141. package/lib/typescript/module/view/glyph-run.d.ts.map +1 -0
  142. package/lib/typescript/module/view/morph-viewport.d.ts +14 -0
  143. package/lib/typescript/module/view/morph-viewport.d.ts.map +1 -0
  144. package/lib/typescript/module/view/number-lane.d.ts +17 -0
  145. package/lib/typescript/module/view/number-lane.d.ts.map +1 -0
  146. package/lib/typescript/module/view/number-run.d.ts +13 -0
  147. package/lib/typescript/module/view/number-run.d.ts.map +1 -0
  148. package/lib/typescript/module/view/text-run.d.ts +11 -0
  149. package/lib/typescript/module/view/text-run.d.ts.map +1 -0
  150. package/package.json +61 -0
  151. package/src/hooks/use-display-units.ts +18 -0
  152. package/src/hooks/use-inline-auto-width.ts +57 -0
  153. package/src/hooks/use-morph-motion.ts +40 -0
  154. package/src/hooks/use-morph-text-style.ts +45 -0
  155. package/src/hooks/use-numeric-lanes.ts +55 -0
  156. package/src/hooks/use-text-glyphs.ts +56 -0
  157. package/src/index.tsx +98 -0
  158. package/src/model/display-units.ts +28 -0
  159. package/src/model/numeric-lanes.ts +80 -0
  160. package/src/model/text-keys.ts +123 -0
  161. package/src/motion/entry-exit-builders.ts +74 -0
  162. package/src/motion/preset-map.ts +127 -0
  163. package/src/types.ts +60 -0
  164. package/src/view/glyph-run.tsx +47 -0
  165. package/src/view/morph-viewport.tsx +83 -0
  166. package/src/view/number-lane.tsx +128 -0
  167. package/src/view/number-run.tsx +73 -0
  168. package/src/view/text-run.tsx +40 -0
@@ -0,0 +1,43 @@
1
+ import type { StyleProp, TextStyle, ViewStyle } from "react-native";
2
+ import type { ComplexAnimationBuilder, EntryExitAnimationFunction, WithTimingConfig } from "react-native-reanimated";
3
+ export type MorphAnimationPresetName = "default" | "smooth" | "snappy" | "bouncy";
4
+ export type MorphContentVariant = "text" | "number";
5
+ type CubicBezierTuple = readonly [number, number, number, number];
6
+ export type MorphAnimationPreset = {
7
+ readonly duration: number;
8
+ readonly ease: CubicBezierTuple;
9
+ } | {
10
+ readonly type: "spring";
11
+ readonly duration: number;
12
+ readonly bounce: number;
13
+ };
14
+ export type GlyphToken = {
15
+ readonly id: string;
16
+ readonly value: string;
17
+ };
18
+ export type NumericFlowDirection = -1 | 0 | 1;
19
+ export type MotionRecipe = {
20
+ readonly durationMs: number;
21
+ readonly easing: NonNullable<WithTimingConfig["easing"]>;
22
+ readonly layoutTransition: ComplexAnimationBuilder;
23
+ readonly enterTransition: EntryExitAnimationFunction;
24
+ readonly exitTransition: EntryExitAnimationFunction;
25
+ readonly driveNumber: (toValue: number, delayMs?: number) => number;
26
+ };
27
+ export type MorphingTextProps = {
28
+ readonly text: string | number;
29
+ readonly variant?: MorphContentVariant;
30
+ readonly fontSize?: number;
31
+ readonly color?: string;
32
+ readonly style?: StyleProp<TextStyle>;
33
+ readonly containerStyle?: StyleProp<ViewStyle>;
34
+ readonly fontStyle?: StyleProp<TextStyle>;
35
+ readonly animationDuration?: number;
36
+ readonly animationPreset?: MorphAnimationPresetName;
37
+ readonly stagger?: number;
38
+ readonly autoSize?: boolean;
39
+ readonly clipToBounds?: boolean;
40
+ };
41
+ export type LaminarProps = MorphingTextProps;
42
+ export {};
43
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,EACV,uBAAuB,EACvB,0BAA0B,EAC1B,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,MAAM,wBAAwB,GAChC,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,QAAQ,CAAC;AAEb,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEpD,KAAK,gBAAgB,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAElE,MAAM,MAAM,oBAAoB,GAC5B;IACE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEN,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAE9C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,gBAAgB,EAAE,uBAAuB,CAAC;IACnD,QAAQ,CAAC,eAAe,EAAE,0BAA0B,CAAC;IACrD,QAAQ,CAAC,cAAc,EAAE,0BAA0B,CAAC;IACpD,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CACrE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,eAAe,CAAC,EAAE,wBAAwB,CAAC;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC"}
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { type StyleProp, type TextStyle } from "react-native";
3
+ import type { ComplexAnimationBuilder, EntryExitAnimationFunction } from "react-native-reanimated";
4
+ import type { GlyphToken } from "../types";
5
+ export declare const GlyphRun: React.MemoExoticComponent<({ glyphs, layoutTransition, enterTransition, exitTransition, textStyle, }: Readonly<{
6
+ glyphs: readonly GlyphToken[];
7
+ layoutTransition: ComplexAnimationBuilder;
8
+ enterTransition?: EntryExitAnimationFunction;
9
+ exitTransition?: EntryExitAnimationFunction;
10
+ textStyle?: StyleProp<TextStyle>;
11
+ }>) => React.JSX.Element>;
12
+ //# sourceMappingURL=glyph-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glyph-run.d.ts","sourceRoot":"","sources":["../../../../src/view/glyph-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EACV,uBAAuB,EACvB,0BAA0B,EAC3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAQ3C,eAAO,MAAM,QAAQ,wGAOhB,QAAQ,CAAC;IACV,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;IAC9B,gBAAgB,EAAE,uBAAuB,CAAC;IAC1C,eAAe,CAAC,EAAE,0BAA0B,CAAC;IAC7C,cAAc,CAAC,EAAE,0BAA0B,CAAC;IAC5C,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAClC,CAAC,uBAgBH,CAAC"}
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import type { StyleProp, ViewStyle } from "react-native";
3
+ import Animated from "react-native-reanimated";
4
+ type MorphViewportProps = {
5
+ readonly autoSize: boolean;
6
+ readonly clipToBounds: boolean;
7
+ readonly containerStyle?: StyleProp<ViewStyle>;
8
+ readonly animatedWidthStyle?: React.ComponentProps<typeof Animated.View>["style"];
9
+ readonly measurement?: React.ReactNode;
10
+ readonly children: React.ReactNode;
11
+ };
12
+ export declare const MorphViewport: React.MemoExoticComponent<({ autoSize, clipToBounds, containerStyle, animatedWidthStyle, measurement, children, }: MorphViewportProps) => React.JSX.Element>;
13
+ export {};
14
+ //# sourceMappingURL=morph-viewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"morph-viewport.d.ts","sourceRoot":"","sources":["../../../../src/view/morph-viewport.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,QAAQ,MAAM,yBAAyB,CAAC;AA4B/C,KAAK,kBAAkB,GAAG;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,aAAa,qHAQrB,kBAAkB,uBAgCtB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import { type StyleProp, type TextStyle } from "react-native";
3
+ import type { MotionRecipe, NumericFlowDirection } from "../types";
4
+ type NumberLaneProps = {
5
+ readonly unit: string;
6
+ readonly tokenKey: number;
7
+ readonly isLead: boolean;
8
+ readonly hasAnimated: boolean;
9
+ readonly delayMs: number;
10
+ readonly direction: NumericFlowDirection;
11
+ readonly travelDistance: number;
12
+ readonly motionRecipe: MotionRecipe;
13
+ readonly textStyle?: StyleProp<TextStyle>;
14
+ };
15
+ export declare const NumberLane: React.MemoExoticComponent<({ unit, tokenKey, isLead, hasAnimated, delayMs, direction, travelDistance, motionRecipe, textStyle, }: NumberLaneProps) => React.JSX.Element>;
16
+ export {};
17
+ //# sourceMappingURL=number-lane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-lane.d.ts","sourceRoot":"","sources":["../../../../src/view/number-lane.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AACvC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAiBnE,KAAK,eAAe,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,oBAAoB,CAAC;IACzC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC3C,CAAC;AAEF,eAAO,MAAM,UAAU,oIAWlB,eAAe,uBAgFnB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { type StyleProp, type TextStyle } from "react-native";
3
+ import type { MotionRecipe } from "../types";
4
+ type NumberRunProps = {
5
+ readonly value: string;
6
+ readonly motionRecipe: MotionRecipe;
7
+ readonly fontSize?: number;
8
+ readonly textStyle?: StyleProp<TextStyle>;
9
+ readonly staggerMs: number;
10
+ };
11
+ export declare const NumberRun: React.MemoExoticComponent<({ value, motionRecipe, fontSize, textStyle, staggerMs, }: Readonly<NumberRunProps>) => React.JSX.Element>;
12
+ export {};
13
+ //# sourceMappingURL=number-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-run.d.ts","sourceRoot":"","sources":["../../../../src/view/number-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAS7C,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,SAAS,uFAOjB,QAAQ,CAAC,cAAc,CAAC,uBA2C5B,CAAC"}
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import type { StyleProp, TextStyle } from "react-native";
3
+ import type { MotionRecipe } from "../types";
4
+ type TextRunProps = {
5
+ readonly value: string;
6
+ readonly motionRecipe: MotionRecipe;
7
+ readonly textStyle?: StyleProp<TextStyle>;
8
+ };
9
+ export declare const TextRun: React.MemoExoticComponent<({ value, motionRecipe, textStyle }: TextRunProps) => React.JSX.Element>;
10
+ export {};
11
+ //# sourceMappingURL=text-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-run.d.ts","sourceRoot":"","sources":["../../../../src/view/text-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC3C,CAAC;AAEF,eAAO,MAAM,OAAO,iEACmB,YAAY,uBAwBlD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "react-native-laminar",
3
+ "version": "1.0.0",
4
+ "description": "A React Native component for morphing text and numbers with character-level identity.",
5
+ "keywords": [
6
+ "react-native",
7
+ "animation",
8
+ "text",
9
+ "morphing",
10
+ "reanimated"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/cajaun/laminar"
15
+ },
16
+ "license": "MIT",
17
+ "source": "./src/index.tsx",
18
+ "main": "./lib/commonjs/index.js",
19
+ "module": "./lib/module/index.js",
20
+ "types": "./lib/typescript/commonjs/index.d.ts",
21
+ "react-native": "./src/index.tsx",
22
+ "exports": {
23
+ ".": {
24
+ "import": {
25
+ "types": "./lib/typescript/module/index.d.ts",
26
+ "default": "./lib/module/index.js"
27
+ },
28
+ "require": {
29
+ "types": "./lib/typescript/commonjs/index.d.ts",
30
+ "default": "./lib/commonjs/index.js"
31
+ }
32
+ }
33
+ },
34
+ "files": [
35
+ "src",
36
+ "lib"
37
+ ],
38
+ "scripts": {
39
+ "build": "bob build",
40
+ "prepare": "bob build"
41
+ },
42
+ "sideEffects": false,
43
+ "peerDependencies": {
44
+ "react": ">=18",
45
+ "react-native": ">=0.74",
46
+ "react-native-reanimated": ">=3"
47
+ },
48
+ "devDependencies": {
49
+ "react-native-builder-bob": "*",
50
+ "typescript": "*"
51
+ },
52
+ "react-native-builder-bob": {
53
+ "source": "src",
54
+ "output": "lib",
55
+ "targets": [
56
+ "commonjs",
57
+ ["module", { "esm": true }],
58
+ "typescript"
59
+ ]
60
+ }
61
+ }
@@ -0,0 +1,18 @@
1
+ import { useMemo } from "react";
2
+ import {
3
+ normalizeDisplayUnit,
4
+ splitDisplayUnits,
5
+ } from "../model/display-units";
6
+
7
+ export const useDisplayUnits = (
8
+ value: string,
9
+ enabled = true
10
+ ): readonly string[] =>
11
+ useMemo(
12
+ () =>
13
+ // skip unit work when auto size is off
14
+ enabled
15
+ ? splitDisplayUnits(value).map(normalizeDisplayUnit)
16
+ : [],
17
+ [enabled, value]
18
+ );
@@ -0,0 +1,57 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import type { LayoutChangeEvent } from "react-native";
3
+ import { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
4
+
5
+ type Params = {
6
+ enabled: boolean;
7
+ driveToWidth: (toValue: number) => number;
8
+ };
9
+
10
+ export const useInlineAutoWidth = ({ enabled, driveToWidth }: Params) => {
11
+ const widthValue = useSharedValue(0);
12
+ const measuredWidthRef = useRef<number | null>(null);
13
+ const bootstrappedRef = useRef(false);
14
+ const [hasBootstrappedWidth, setHasBootstrappedWidth] = useState(false);
15
+
16
+ const captureLayout = useCallback(
17
+ (event: LayoutChangeEvent) => {
18
+ if (!enabled) {
19
+ return;
20
+ }
21
+
22
+ const nextWidth = Math.max(0, Math.ceil(event.nativeEvent.layout.width));
23
+
24
+ if (measuredWidthRef.current === nextWidth) {
25
+ return;
26
+ }
27
+
28
+ measuredWidthRef.current = nextWidth;
29
+
30
+ // snap the first width so mount does not animate from zero
31
+ if (!bootstrappedRef.current) {
32
+ bootstrappedRef.current = true;
33
+ widthValue.value = nextWidth;
34
+ setHasBootstrappedWidth(true);
35
+ return;
36
+ }
37
+
38
+ widthValue.value = driveToWidth(nextWidth);
39
+ },
40
+ [driveToWidth, enabled, widthValue]
41
+ );
42
+
43
+ const animatedWidthStyle = useAnimatedStyle(
44
+ () =>
45
+ enabled && hasBootstrappedWidth
46
+ ? {
47
+ width: widthValue.value,
48
+ }
49
+ : {},
50
+ [enabled, hasBootstrappedWidth]
51
+ );
52
+
53
+ return {
54
+ captureLayout,
55
+ animatedWidthStyle,
56
+ };
57
+ };
@@ -0,0 +1,40 @@
1
+ import { useMemo } from "react";
2
+ import { resolveMotionRecipe } from "../motion/preset-map";
3
+ import type {
4
+ MotionRecipe,
5
+ MorphAnimationPresetName,
6
+ MorphContentVariant,
7
+ } from "../types";
8
+
9
+ type Params = {
10
+ readonly variant: MorphContentVariant;
11
+ readonly animationPreset?: MorphAnimationPresetName;
12
+ readonly animationDuration?: number;
13
+ readonly stagger: number;
14
+ };
15
+
16
+ type MorphMotion = {
17
+ readonly motionRecipe: MotionRecipe;
18
+ readonly staggerMs: number;
19
+ };
20
+
21
+ export const useMorphMotion = ({
22
+ variant,
23
+ animationPreset,
24
+ animationDuration,
25
+ stagger,
26
+ }: Params): MorphMotion => {
27
+ // numbers feel better with snappy unless you pick something else
28
+ const resolvedPreset =
29
+ animationPreset ?? (variant === "text" ? "default" : "snappy");
30
+
31
+ const motionRecipe = useMemo(
32
+ () => resolveMotionRecipe(resolvedPreset, animationDuration),
33
+ [animationDuration, resolvedPreset]
34
+ );
35
+
36
+ return {
37
+ motionRecipe,
38
+ staggerMs: Math.round(stagger * 1000),
39
+ };
40
+ };
@@ -0,0 +1,45 @@
1
+ import { useMemo } from "react";
2
+ import type { StyleProp, TextStyle } from "react-native";
3
+
4
+ type Params = {
5
+ readonly fontSize?: number;
6
+ readonly color?: string;
7
+ readonly fontStyle?: StyleProp<TextStyle>;
8
+ readonly style?: StyleProp<TextStyle>;
9
+ };
10
+
11
+ type MorphTextStyle = {
12
+ readonly textStyle: StyleProp<TextStyle>;
13
+ };
14
+
15
+ export const useMorphTextStyle = ({
16
+ fontSize,
17
+ color,
18
+ fontStyle,
19
+ style,
20
+ }: Params): MorphTextStyle => {
21
+ const baseTextStyle = useMemo(() => {
22
+ const nextStyle: TextStyle = {
23
+ includeFontPadding: false,
24
+ };
25
+
26
+ if (fontSize !== undefined) {
27
+ nextStyle.fontSize = fontSize;
28
+ }
29
+
30
+ if (color !== undefined) {
31
+ nextStyle.color = color;
32
+ }
33
+
34
+ return nextStyle;
35
+ }, [color, fontSize]);
36
+
37
+ const textStyle = useMemo(
38
+ () => [baseTextStyle, fontStyle, style],
39
+ [baseTextStyle, fontStyle, style]
40
+ );
41
+
42
+ return {
43
+ textStyle,
44
+ };
45
+ };
@@ -0,0 +1,55 @@
1
+ import { useMemo, useRef } from "react";
2
+ import type { NumericFlowDirection } from "../types";
3
+ import {
4
+ findNumericLeadLength,
5
+ splitDisplayUnits,
6
+ } from "../model/display-units";
7
+ import { reconcileNumericLanes } from "../model/numeric-lanes";
8
+
9
+ type NumericLedger = {
10
+ previousValue: string;
11
+ laneKeys: readonly number[];
12
+ nextSeed: number;
13
+ direction: NumericFlowDirection;
14
+ };
15
+
16
+ type NumericLaneSnapshot = {
17
+ readonly units: readonly string[];
18
+ readonly laneKeys: readonly number[];
19
+ readonly direction: NumericFlowDirection;
20
+ readonly leadLength: number;
21
+ };
22
+
23
+ export const useNumericLanes = (value: string): NumericLaneSnapshot => {
24
+ const units = useMemo(() => splitDisplayUnits(value), [value]);
25
+ const ledgerRef = useRef<NumericLedger>({
26
+ previousValue: value,
27
+ laneKeys: units.map((_, index) => index),
28
+ nextSeed: units.length,
29
+ direction: 0,
30
+ });
31
+
32
+ // keep lane ids in a ref so the number can reflow without remounting everything
33
+ if (value !== ledgerRef.current.previousValue) {
34
+ const nextLedger = reconcileNumericLanes(
35
+ ledgerRef.current.previousValue,
36
+ value,
37
+ ledgerRef.current.laneKeys,
38
+ ledgerRef.current.nextSeed
39
+ );
40
+
41
+ ledgerRef.current = {
42
+ previousValue: value,
43
+ laneKeys: nextLedger.laneKeys,
44
+ nextSeed: nextLedger.nextSeed,
45
+ direction: nextLedger.direction,
46
+ };
47
+ }
48
+
49
+ return {
50
+ units,
51
+ laneKeys: ledgerRef.current.laneKeys,
52
+ direction: ledgerRef.current.direction,
53
+ leadLength: findNumericLeadLength(units),
54
+ };
55
+ };
@@ -0,0 +1,56 @@
1
+ import { useMemo, useRef } from "react";
2
+ import {
3
+ normalizeDisplayUnit,
4
+ splitDisplayUnits,
5
+ } from "../model/display-units";
6
+ import { reconcileTextGlyphKeys } from "../model/text-keys";
7
+ import type { GlyphToken } from "../types";
8
+
9
+ type TextGlyphLedger = {
10
+ previousValue: string;
11
+ previousUnits: readonly string[];
12
+ glyphKeys: readonly string[];
13
+ nextSeed: number;
14
+ };
15
+
16
+ export const useTextGlyphs = (
17
+ value: string,
18
+ namespace: string
19
+ ): readonly GlyphToken[] => {
20
+ const units = useMemo(() => splitDisplayUnits(value), [value]);
21
+ const ledgerRef = useRef<TextGlyphLedger>({
22
+ previousValue: value,
23
+ previousUnits: units,
24
+ glyphKeys: units.map((_, index) => `${namespace}:c${index}`),
25
+ nextSeed: units.length,
26
+ });
27
+
28
+ if (value !== ledgerRef.current.previousValue) {
29
+ // keep ids stable so unchanged glyphs stay mounted between updates
30
+ const nextLedger = reconcileTextGlyphKeys(
31
+ ledgerRef.current.previousUnits,
32
+ units,
33
+ ledgerRef.current.glyphKeys,
34
+ ledgerRef.current.nextSeed,
35
+ namespace
36
+ );
37
+
38
+ ledgerRef.current = {
39
+ previousValue: value,
40
+ previousUnits: units,
41
+ glyphKeys: nextLedger.glyphKeys,
42
+ nextSeed: nextLedger.nextSeed,
43
+ };
44
+ }
45
+
46
+ const glyphKeys = ledgerRef.current.glyphKeys;
47
+
48
+ return useMemo(
49
+ () =>
50
+ units.map((unit, index) => ({
51
+ id: glyphKeys[index],
52
+ value: normalizeDisplayUnit(unit),
53
+ })),
54
+ [glyphKeys, units]
55
+ );
56
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,98 @@
1
+ import React from "react";
2
+ import { Text } from "react-native";
3
+ import { useInlineAutoWidth } from "./hooks/use-inline-auto-width";
4
+ import { useMorphMotion } from "./hooks/use-morph-motion";
5
+ import { useMorphTextStyle } from "./hooks/use-morph-text-style";
6
+ import {
7
+ normalizeDisplayUnit,
8
+ splitDisplayUnits,
9
+ } from "./model/display-units";
10
+ import type { MorphingTextProps } from "./types";
11
+ import { MorphViewport } from "./view/morph-viewport";
12
+ import { NumberRun } from "./view/number-run";
13
+ import { TextRun } from "./view/text-run";
14
+
15
+ export const Laminar = React.memo(function Laminar({
16
+ text,
17
+ variant = "text",
18
+ fontSize,
19
+ color,
20
+ style,
21
+ containerStyle,
22
+ fontStyle,
23
+ animationDuration,
24
+ animationPreset,
25
+ stagger = 0.02,
26
+ autoSize = true,
27
+ clipToBounds = false,
28
+ }: Readonly<MorphingTextProps>) {
29
+ const resolvedValue = String(text ?? "");
30
+ const { motionRecipe, staggerMs } = useMorphMotion({
31
+ variant,
32
+ animationPreset,
33
+ animationDuration,
34
+ stagger,
35
+ });
36
+ const { textStyle } = useMorphTextStyle({
37
+ fontSize,
38
+ color,
39
+ fontStyle,
40
+ style,
41
+ });
42
+
43
+ const { captureLayout, animatedWidthStyle } = useInlineAutoWidth({
44
+ enabled: autoSize,
45
+ driveToWidth: motionRecipe.driveNumber,
46
+ });
47
+ const measuredValue = React.useMemo(() => {
48
+ if (!autoSize) {
49
+ return "";
50
+ }
51
+
52
+ return splitDisplayUnits(resolvedValue).map(normalizeDisplayUnit).join("");
53
+ }, [autoSize, resolvedValue]);
54
+
55
+ return (
56
+ <MorphViewport
57
+ autoSize={autoSize}
58
+ clipToBounds={clipToBounds}
59
+ containerStyle={containerStyle}
60
+ animatedWidthStyle={animatedWidthStyle}
61
+ measurement={
62
+ <Text
63
+ numberOfLines={1}
64
+ onLayout={captureLayout}
65
+ style={textStyle}
66
+ >
67
+ {measuredValue}
68
+ </Text>
69
+ }
70
+ >
71
+ {variant === "number" ? (
72
+ <NumberRun
73
+ value={resolvedValue}
74
+ motionRecipe={motionRecipe}
75
+ fontSize={fontSize}
76
+ textStyle={textStyle}
77
+ staggerMs={staggerMs}
78
+ />
79
+ ) : (
80
+ <TextRun
81
+ value={resolvedValue}
82
+ motionRecipe={motionRecipe}
83
+ textStyle={textStyle}
84
+ />
85
+ )}
86
+ </MorphViewport>
87
+ );
88
+ });
89
+
90
+ export const MorphingText = Laminar;
91
+
92
+ export default Laminar;
93
+ export type {
94
+ LaminarProps,
95
+ MorphAnimationPresetName,
96
+ MorphContentVariant,
97
+ MorphingTextProps,
98
+ } from "./types";
@@ -0,0 +1,28 @@
1
+ const NBSP = "\u00A0";
2
+ const graphemeSegmenter =
3
+ typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
4
+ ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
5
+ : null;
6
+
7
+ export const splitDisplayUnits = (input: string): string[] => {
8
+ if (graphemeSegmenter) {
9
+ return Array.from(
10
+ graphemeSegmenter.segment(input),
11
+ (part) => part.segment
12
+ );
13
+ }
14
+
15
+ return Array.from(input);
16
+ };
17
+
18
+ // swap plain spaces so measured text keeps its width
19
+ export const normalizeDisplayUnit = (unit: string) =>
20
+ unit === " " ? NBSP : unit;
21
+
22
+ export const isAsciiDigit = (unit: string) => unit >= "0" && unit <= "9";
23
+
24
+ export const findNumericLeadLength = (units: readonly string[]) => {
25
+ const firstDigitIndex = units.findIndex(isAsciiDigit);
26
+
27
+ return firstDigitIndex === -1 ? units.length : firstDigitIndex;
28
+ };