react-native-laminar 1.4.4 → 1.5.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 (109) hide show
  1. package/README.md +36 -6
  2. package/lib/commonjs/hooks/use-inline-auto-width.js +24 -1
  3. package/lib/commonjs/hooks/use-inline-auto-width.js.map +1 -1
  4. package/lib/commonjs/hooks/use-leading-animation.js +49 -0
  5. package/lib/commonjs/hooks/use-leading-animation.js.map +1 -0
  6. package/lib/commonjs/hooks/use-morph-motion.js +3 -1
  7. package/lib/commonjs/hooks/use-morph-motion.js.map +1 -1
  8. package/lib/commonjs/index.js +35 -6
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/motion/preset-map.js +5 -0
  11. package/lib/commonjs/motion/preset-map.js.map +1 -1
  12. package/lib/commonjs/view/leading-element.js +37 -0
  13. package/lib/commonjs/view/leading-element.js.map +1 -0
  14. package/lib/commonjs/view/morph-viewport.js +26 -0
  15. package/lib/commonjs/view/morph-viewport.js.map +1 -1
  16. package/lib/commonjs/view/number-run.js +70 -22
  17. package/lib/commonjs/view/number-run.js.map +1 -1
  18. package/lib/commonjs/view/slot-shadow.js +77 -0
  19. package/lib/commonjs/view/slot-shadow.js.map +1 -0
  20. package/lib/commonjs/view/slots-run.js +111 -42
  21. package/lib/commonjs/view/slots-run.js.map +1 -1
  22. package/lib/commonjs/view/text-run.js +13 -24
  23. package/lib/commonjs/view/text-run.js.map +1 -1
  24. package/lib/commonjs/view/ticker-run.js +127 -0
  25. package/lib/commonjs/view/ticker-run.js.map +1 -0
  26. package/lib/module/hooks/use-inline-auto-width.js +24 -1
  27. package/lib/module/hooks/use-inline-auto-width.js.map +1 -1
  28. package/lib/module/hooks/use-leading-animation.js +44 -0
  29. package/lib/module/hooks/use-leading-animation.js.map +1 -0
  30. package/lib/module/hooks/use-morph-motion.js +3 -1
  31. package/lib/module/hooks/use-morph-motion.js.map +1 -1
  32. package/lib/module/index.js +35 -6
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/motion/preset-map.js +5 -0
  35. package/lib/module/motion/preset-map.js.map +1 -1
  36. package/lib/module/view/leading-element.js +32 -0
  37. package/lib/module/view/leading-element.js.map +1 -0
  38. package/lib/module/view/morph-viewport.js +28 -2
  39. package/lib/module/view/morph-viewport.js.map +1 -1
  40. package/lib/module/view/number-run.js +70 -23
  41. package/lib/module/view/number-run.js.map +1 -1
  42. package/lib/module/view/slot-shadow.js +72 -0
  43. package/lib/module/view/slot-shadow.js.map +1 -0
  44. package/lib/module/view/slots-run.js +111 -42
  45. package/lib/module/view/slots-run.js.map +1 -1
  46. package/lib/module/view/text-run.js +14 -25
  47. package/lib/module/view/text-run.js.map +1 -1
  48. package/lib/module/view/ticker-run.js +121 -0
  49. package/lib/module/view/ticker-run.js.map +1 -0
  50. package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/hooks/use-leading-animation.d.ts +16 -0
  52. package/lib/typescript/commonjs/hooks/use-leading-animation.d.ts.map +1 -0
  53. package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts +3 -2
  54. package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/index.d.ts +1 -1
  56. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/motion/preset-map.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/types.d.ts +14 -2
  59. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/view/leading-element.d.ts +14 -0
  61. package/lib/typescript/commonjs/view/leading-element.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/view/morph-viewport.d.ts +4 -1
  63. package/lib/typescript/commonjs/view/morph-viewport.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/view/number-run.d.ts +6 -2
  65. package/lib/typescript/commonjs/view/number-run.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/view/slot-shadow.d.ts +11 -0
  67. package/lib/typescript/commonjs/view/slot-shadow.d.ts.map +1 -0
  68. package/lib/typescript/commonjs/view/slots-run.d.ts +8 -3
  69. package/lib/typescript/commonjs/view/slots-run.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/view/text-run.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/view/ticker-run.d.ts +21 -0
  72. package/lib/typescript/commonjs/view/ticker-run.d.ts.map +1 -0
  73. package/lib/typescript/module/hooks/use-inline-auto-width.d.ts.map +1 -1
  74. package/lib/typescript/module/hooks/use-leading-animation.d.ts +16 -0
  75. package/lib/typescript/module/hooks/use-leading-animation.d.ts.map +1 -0
  76. package/lib/typescript/module/hooks/use-morph-motion.d.ts +3 -2
  77. package/lib/typescript/module/hooks/use-morph-motion.d.ts.map +1 -1
  78. package/lib/typescript/module/index.d.ts +1 -1
  79. package/lib/typescript/module/index.d.ts.map +1 -1
  80. package/lib/typescript/module/motion/preset-map.d.ts.map +1 -1
  81. package/lib/typescript/module/types.d.ts +14 -2
  82. package/lib/typescript/module/types.d.ts.map +1 -1
  83. package/lib/typescript/module/view/leading-element.d.ts +14 -0
  84. package/lib/typescript/module/view/leading-element.d.ts.map +1 -0
  85. package/lib/typescript/module/view/morph-viewport.d.ts +4 -1
  86. package/lib/typescript/module/view/morph-viewport.d.ts.map +1 -1
  87. package/lib/typescript/module/view/number-run.d.ts +6 -2
  88. package/lib/typescript/module/view/number-run.d.ts.map +1 -1
  89. package/lib/typescript/module/view/slot-shadow.d.ts +11 -0
  90. package/lib/typescript/module/view/slot-shadow.d.ts.map +1 -0
  91. package/lib/typescript/module/view/slots-run.d.ts +8 -3
  92. package/lib/typescript/module/view/slots-run.d.ts.map +1 -1
  93. package/lib/typescript/module/view/text-run.d.ts.map +1 -1
  94. package/lib/typescript/module/view/ticker-run.d.ts +21 -0
  95. package/lib/typescript/module/view/ticker-run.d.ts.map +1 -0
  96. package/package.json +3 -1
  97. package/src/hooks/use-inline-auto-width.ts +33 -1
  98. package/src/hooks/use-leading-animation.ts +74 -0
  99. package/src/hooks/use-morph-motion.ts +10 -1
  100. package/src/index.tsx +44 -6
  101. package/src/motion/preset-map.ts +5 -0
  102. package/src/types.ts +17 -2
  103. package/src/view/leading-element.tsx +45 -0
  104. package/src/view/morph-viewport.tsx +39 -1
  105. package/src/view/number-run.tsx +94 -26
  106. package/src/view/slot-shadow.tsx +99 -0
  107. package/src/view/slots-run.tsx +163 -58
  108. package/src/view/text-run.tsx +14 -51
  109. package/src/view/ticker-run.tsx +167 -0
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { type ReactNode } from "react";
2
2
  import { type StyleProp, type TextStyle } from "react-native";
3
3
  import type { LaminarAlign, MotionRecipe } from "../types";
4
4
  type NumberRunProps = {
@@ -9,7 +9,11 @@ type NumberRunProps = {
9
9
  readonly textStyle?: StyleProp<TextStyle>;
10
10
  readonly staggerMs: number;
11
11
  readonly className?: string;
12
+ readonly leading?: ReactNode;
13
+ readonly leadingKey?: string | number;
14
+ readonly leadingGap?: number;
15
+ readonly ready?: boolean;
12
16
  };
13
- export declare const NumberRun: React.MemoExoticComponent<({ value, motionRecipe, align, fontSize, textStyle, staggerMs, className, }: Readonly<NumberRunProps>) => React.JSX.Element>;
17
+ export declare const NumberRun: React.MemoExoticComponent<({ value, motionRecipe, align, fontSize, textStyle, staggerMs, className, leading, leadingKey, leadingGap, ready, }: Readonly<NumberRunProps>) => React.JSX.Element>;
14
18
  export {};
15
19
  //# sourceMappingURL=number-run.d.ts.map
@@ -1 +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,YAAY,EAAE,MAAM,UAAU,CAAC;AAe3D,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,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;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,SAAS,yGASjB,QAAQ,CAAC,cAAc,CAAC,uBA8C5B,CAAC"}
1
+ {"version":3,"file":"number-run.d.ts","sourceRoot":"","sources":["../../../../src/view/number-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAmB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/D,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAgB3D,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,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;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAGF,eAAO,MAAM,SAAS,iJAajB,QAAQ,CAAC,cAAc,CAAC,uBAuG5B,CAAC"}
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ type SlotShadowProps = {
3
+ readonly slotHeight: number;
4
+ readonly viewportHeight: number;
5
+ readonly color?: string;
6
+ readonly size?: number;
7
+ readonly children: React.ReactElement;
8
+ };
9
+ export declare const SlotShadow: React.NamedExoticComponent<SlotShadowProps>;
10
+ export {};
11
+ //# sourceMappingURL=slot-shadow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slot-shadow.d.ts","sourceRoot":"","sources":["../../../../src/view/slot-shadow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAkD1B,KAAK,eAAe,GAAG;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC;CACvC,CAAC;AAIF,eAAO,MAAM,UAAU,6CA+BrB,CAAC"}
@@ -1,6 +1,6 @@
1
- import React from "react";
1
+ import React, { type ReactNode } from "react";
2
2
  import { type StyleProp, type TextStyle } from "react-native";
3
- import type { LaminarAlign, MotionRecipe } from "../types";
3
+ import type { LaminarAlign, LaminarShadow, MotionRecipe } from "../types";
4
4
  type SlotsRunProps = {
5
5
  readonly value: string;
6
6
  readonly motionRecipe: MotionRecipe;
@@ -9,7 +9,12 @@ type SlotsRunProps = {
9
9
  readonly textStyle?: StyleProp<TextStyle>;
10
10
  readonly staggerMs: number;
11
11
  readonly className?: string;
12
+ readonly leading?: ReactNode;
13
+ readonly leadingKey?: string | number;
14
+ readonly leadingGap?: number;
15
+ readonly ready?: boolean;
16
+ readonly shadow?: LaminarShadow;
12
17
  };
13
- export declare const SlotsRun: React.MemoExoticComponent<({ value, motionRecipe, align, fontSize, textStyle, staggerMs, className, }: Readonly<SlotsRunProps>) => React.JSX.Element>;
18
+ export declare const SlotsRun: React.MemoExoticComponent<({ value, motionRecipe, align, fontSize, textStyle, staggerMs, className, leading, leadingKey, leadingGap, ready, shadow, }: Readonly<SlotsRunProps>) => React.JSX.Element>;
14
19
  export {};
15
20
  //# sourceMappingURL=slots-run.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slots-run.d.ts","sourceRoot":"","sources":["../../../../src/view/slots-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqC,MAAM,OAAO,CAAC;AAC1D,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,SAAS,EAEf,MAAM,cAAc,CAAC;AAQtB,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAmQjF,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,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;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,QAAQ,yGAShB,QAAQ,CAAC,aAAa,CAAC,uBAwF3B,CAAC"}
1
+ {"version":3,"file":"slots-run.d.ts","sourceRoot":"","sources":["../../../../src/view/slots-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAA8B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1E,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,SAAS,EAEf,MAAM,cAAc,CAAC;AAStB,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EAEb,MAAM,UAAU,CAAC;AAqSlB,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,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;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC,CAAC;AAGF,eAAO,MAAM,QAAQ,yJAchB,QAAQ,CAAC,aAAa,CAAC,uBA+I3B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"text-run.d.ts","sourceRoot":"","sources":["../../../../src/view/text-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkD,MAAM,OAAO,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG3D,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,OAAO,gJAYf,YAAY,uBAsHhB,CAAC"}
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,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG3D,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,OAAO,gJAYf,YAAY,uBAqFhB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { type StyleProp, type TextStyle } from "react-native";
3
+ import type { LaminarAlign, LaminarShadow, MotionRecipe } from "../types";
4
+ type TickerRunProps = {
5
+ readonly value: string;
6
+ readonly motionRecipe: MotionRecipe;
7
+ readonly align: LaminarAlign;
8
+ readonly fontSize?: number;
9
+ readonly textStyle?: StyleProp<TextStyle>;
10
+ readonly staggerMs: number;
11
+ readonly className?: string;
12
+ readonly leading?: ReactNode;
13
+ readonly leadingKey?: string | number;
14
+ readonly leadingGap?: number;
15
+ readonly ready?: boolean;
16
+ /** accepted for API compatibility; flow values stay crisp and unmasked */
17
+ readonly shadow?: LaminarShadow;
18
+ };
19
+ export declare const TickerRun: React.MemoExoticComponent<({ value, motionRecipe, align, textStyle, className, leading, leadingKey, leadingGap, ready, }: Readonly<TickerRunProps>) => React.JSX.Element>;
20
+ export {};
21
+ //# sourceMappingURL=ticker-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticker-run.d.ts","sourceRoot":"","sources":["../../../../src/view/ticker-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAKpE,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EACb,MAAM,UAAU,CAAC;AAsBlB,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,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;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC,CAAC;AAKF,eAAO,MAAM,SAAS,4HAWjB,QAAQ,CAAC,cAAc,CAAC,uBAsG5B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-laminar",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
4
4
  "description": "A React Native component for morphing text and numbers with character-level identity.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -50,6 +50,8 @@
50
50
  },
51
51
  "sideEffects": false,
52
52
  "peerDependencies": {
53
+ "@react-native-masked-view/masked-view": ">=0.3",
54
+ "expo-linear-gradient": ">=15",
53
55
  "react": ">=18",
54
56
  "react-native": ">=0.74",
55
57
  "react-native-reanimated": ">=3"
@@ -32,6 +32,7 @@ export const useInlineAutoWidth = ({
32
32
  const measuredWidthRef = useRef<number | null>(null);
33
33
  const bootstrappedRef = useRef(false);
34
34
  const widthCacheRef = useRef(new Map<string, number>());
35
+ const firstEnabledMeasurementKeyRef = useRef<string | null>(null);
35
36
  const hasCapturedVisibleLayoutRef = useRef(false);
36
37
  const initialMeasurementKeyRef = useRef<string | null>(null);
37
38
  const firstProbeRef = useRef<PendingProbe | null>(null);
@@ -41,6 +42,11 @@ export const useInlineAutoWidth = ({
41
42
  null
42
43
  );
43
44
 
45
+ // latch the first enabled key so a pending visible layout cannot use a later key
46
+ if (enabled && firstEnabledMeasurementKeyRef.current === null) {
47
+ firstEnabledMeasurementKeyRef.current = measurementKey;
48
+ }
49
+
44
50
  const markMeasurementReady = useCallback((key: string) => {
45
51
  setReadyMeasurementKey((currentKey) =>
46
52
  currentKey === key ? currentKey : key
@@ -111,11 +117,37 @@ export const useInlineAutoWidth = ({
111
117
  // bootstrap from the visible tree so the first explicit width matches
112
118
  // the geometry that was already painted to the user
113
119
  const nextWidth = Math.max(0, event.nativeEvent.layout.width);
120
+ const firstEnabledMeasurementKey =
121
+ firstEnabledMeasurementKeyRef.current;
122
+ const visibleKeyMatchesInitialKey =
123
+ firstEnabledMeasurementKey === null ||
124
+ firstEnabledMeasurementKey === measurementKey;
125
+ const firstProbe = firstProbeRef.current;
126
+
127
+ if (!visibleKeyMatchesInitialKey) {
128
+ // ignore a visible layout from the initial row when the key has changed
129
+ const currentProbeWidth =
130
+ firstProbe?.key === measurementKey
131
+ ? firstProbe.width
132
+ : widthCacheRef.current.get(measurementKey);
133
+
134
+ if (currentProbeWidth !== undefined) {
135
+ initialMeasurementKeyRef.current = measurementKey;
136
+ applyWidth(currentProbeWidth);
137
+ markMeasurementReady(measurementKey);
138
+ } else if (firstProbe) {
139
+ // use the old probe as the shell baseline until the current key is measured
140
+ initialMeasurementKeyRef.current = firstProbe.key;
141
+ applyWidth(firstProbe.width);
142
+ }
143
+
144
+ return;
145
+ }
146
+
114
147
  // retain the visible width for the initial key so a later cache hit does
115
148
  // not reintroduce the probe-to-visible mismatch
116
149
  widthCacheRef.current.delete(measurementKey);
117
150
  widthCacheRef.current.set(measurementKey, nextWidth);
118
- const firstProbe = firstProbeRef.current;
119
151
  if (firstProbe && firstProbe.key !== measurementKey) {
120
152
  initialMeasurementKeyRef.current = firstProbe.key;
121
153
  applyWidth(firstProbe.width);
@@ -0,0 +1,74 @@
1
+ import { useLayoutEffect, useMemo, useRef, type ReactNode } from "react";
2
+ import { useSharedValue } from "react-native-reanimated";
3
+ import {
4
+ createLeadingEnterTransition,
5
+ createLeadingExitTransition,
6
+ } from "../motion/entry-exit-builders";
7
+ import type { MotionRecipe } from "../types";
8
+
9
+ type UseLeadingAnimationParams = {
10
+ readonly leading?: ReactNode;
11
+ readonly leadingKey?: string | number;
12
+ readonly leadingGap: number;
13
+ readonly motionRecipe: MotionRecipe;
14
+ };
15
+
16
+ // keep leading presence and replacement motion consistent across every content variant
17
+ export const useLeadingAnimation = ({
18
+ leading,
19
+ leadingKey,
20
+ leadingGap,
21
+ motionRecipe,
22
+ }: UseLeadingAnimationParams) => {
23
+ const lastLeadingPresenceRef = useRef(Boolean(leading));
24
+ const lastLeadingKeyRef = useRef(leadingKey);
25
+ const isLeadingSwap =
26
+ Boolean(leading) &&
27
+ lastLeadingPresenceRef.current &&
28
+ leadingKey !== lastLeadingKeyRef.current;
29
+ const isLeadingChange =
30
+ Boolean(leading) !== lastLeadingPresenceRef.current ||
31
+ (Boolean(leading) && leadingKey !== lastLeadingKeyRef.current);
32
+ const leadingSwapProgress = useSharedValue(0);
33
+
34
+ // update the worklet flag after commit so render never writes a shared value
35
+ useLayoutEffect(() => {
36
+ leadingSwapProgress.value = isLeadingSwap ? 1 : 0;
37
+ }, [isLeadingSwap, leadingSwapProgress]);
38
+
39
+ const elementEnterTransition = useMemo(
40
+ () =>
41
+ createLeadingEnterTransition({
42
+ durationMs: motionRecipe.durationMs,
43
+ easing: motionRecipe.easing,
44
+ }),
45
+ [motionRecipe.durationMs, motionRecipe.easing]
46
+ );
47
+ const elementExitTransition = useMemo(
48
+ () =>
49
+ createLeadingExitTransition({
50
+ durationMs: motionRecipe.durationMs,
51
+ easing: motionRecipe.easing,
52
+ leadingGap,
53
+ scale: leadingSwapProgress,
54
+ }),
55
+ [
56
+ leadingGap,
57
+ leadingSwapProgress,
58
+ motionRecipe.durationMs,
59
+ motionRecipe.easing,
60
+ ]
61
+ );
62
+
63
+ if (isLeadingChange) {
64
+ lastLeadingPresenceRef.current = Boolean(leading);
65
+ lastLeadingKeyRef.current = leadingKey;
66
+ }
67
+
68
+ return {
69
+ elementEnterTransition,
70
+ elementExitTransition,
71
+ isLeadingChange,
72
+ isLeadingSwap,
73
+ };
74
+ };
@@ -4,10 +4,12 @@ import type {
4
4
  MotionRecipe,
5
5
  MorphAnimationPresetName,
6
6
  MorphContentVariant,
7
+ MorphNumberMode,
7
8
  } from "../types";
8
9
 
9
10
  type Params = {
10
11
  readonly variant: MorphContentVariant;
12
+ readonly numberMode?: MorphNumberMode;
11
13
  readonly animationPreset?: MorphAnimationPresetName;
12
14
  readonly animationDuration?: number;
13
15
  readonly stagger: number;
@@ -21,13 +23,20 @@ type MorphMotion = {
21
23
  // resolve one motion recipe and one shared stagger unit for a render tree
22
24
  export const useMorphMotion = ({
23
25
  variant,
26
+ numberMode,
24
27
  animationPreset,
25
28
  animationDuration,
26
29
  stagger,
27
30
  }: Params): MorphMotion => {
31
+ const isTicker = variant === "number" && numberMode === "ticker";
28
32
  // numeric variants need a quicker default because their lanes update often
29
33
  const resolvedPreset =
30
- animationPreset ?? (variant === "text" ? "default" : "snappy");
34
+ animationPreset ??
35
+ (isTicker
36
+ ? "ticker"
37
+ : variant === "text"
38
+ ? "default"
39
+ : "snappy");
31
40
 
32
41
  const motionRecipe = useMemo(
33
42
  () => resolveMotionRecipe(resolvedPreset, animationDuration),
package/src/index.tsx CHANGED
@@ -11,6 +11,7 @@ import type { LaminarLeadingMap, MorphingTextProps } from "./types";
11
11
  import { MorphViewport } from "./view/morph-viewport";
12
12
  import { NumberRun } from "./view/number-run";
13
13
  import { SlotsRun } from "./view/slots-run";
14
+ import { TickerRun } from "./view/ticker-run";
14
15
  import { TextRun } from "./view/text-run";
15
16
 
16
17
  // choose the content variant and connect it to the shared viewport and measurement flow
@@ -24,12 +25,14 @@ export const Laminar = React.memo(function Laminar({
24
25
  leading,
25
26
  leadingKey,
26
27
  leadingGap = 0,
28
+ numberMode = "morph",
29
+ shadow = false,
27
30
  style,
28
31
  containerStyle,
29
32
  fontStyle,
30
33
  animationDuration,
31
34
  animationPreset,
32
- stagger = 0.02,
35
+ stagger,
33
36
  autoSize = true,
34
37
  clipToBounds = false,
35
38
  }: Readonly<MorphingTextProps>) {
@@ -40,11 +43,16 @@ export const Laminar = React.memo(function Laminar({
40
43
  ? leadingMap[resolvedValue]
41
44
  : (leading as React.ReactNode);
42
45
  const resolvedLeadingKey = leadingMap ? resolvedValue : leadingKey;
46
+ const resolvedStagger =
47
+ stagger ?? (variant === "number" && numberMode === "ticker" ? 0 : 0.02);
48
+ const useLayoutAutoSize =
49
+ autoSize && variant === "number" && numberMode === "ticker";
43
50
  const { motionRecipe, staggerMs } = useMorphMotion({
44
51
  variant,
52
+ numberMode,
45
53
  animationPreset,
46
54
  animationDuration,
47
- stagger,
55
+ stagger: resolvedStagger,
48
56
  });
49
57
  const { textStyle } = useMorphTextStyle({
50
58
  fontSize,
@@ -67,9 +75,9 @@ export const Laminar = React.memo(function Laminar({
67
75
  flattenedStyle?.letterSpacing,
68
76
  flattenedStyle?.lineHeight,
69
77
  flattenedStyle?.textTransform,
70
- variant === "text" && Boolean(resolvedLeading),
71
- variant === "text" ? resolvedLeadingKey : undefined,
72
- variant === "text" ? leadingGap : 0,
78
+ Boolean(resolvedLeading),
79
+ resolvedLeadingKey,
80
+ leadingGap,
73
81
  ]);
74
82
  }, [
75
83
  leadingGap,
@@ -109,6 +117,9 @@ export const Laminar = React.memo(function Laminar({
109
117
  align={align}
110
118
  containerStyle={containerStyle}
111
119
  animatedWidthStyle={animatedWidthStyle}
120
+ autoSizeLayoutTransition={
121
+ useLayoutAutoSize ? motionRecipe.layoutTransition : undefined
122
+ }
112
123
  onVisibleLayout={autoSize ? captureVisibleLayout : undefined}
113
124
  measurement={
114
125
  shouldMeasure ? (
@@ -117,7 +128,7 @@ export const Laminar = React.memo(function Laminar({
117
128
  onLayout={captureLayout}
118
129
  style={{ flexDirection: "row", alignSelf: "flex-start" }}
119
130
  >
120
- {variant === "text" && resolvedLeading ? (
131
+ {resolvedLeading ? (
121
132
  <View
122
133
  style={{
123
134
  alignItems: "center",
@@ -146,6 +157,26 @@ export const Laminar = React.memo(function Laminar({
146
157
  textStyle={textStyle}
147
158
  staggerMs={staggerMs}
148
159
  className={className}
160
+ leading={resolvedLeading}
161
+ leadingKey={resolvedLeadingKey}
162
+ leadingGap={leadingGap}
163
+ ready={!autoSize || isAutoSizeReady}
164
+ shadow={shadow}
165
+ />
166
+ ) : variant === "number" && numberMode === "ticker" ? (
167
+ <TickerRun
168
+ value={resolvedValue}
169
+ motionRecipe={motionRecipe}
170
+ align={align}
171
+ fontSize={fontSize}
172
+ textStyle={textStyle}
173
+ staggerMs={staggerMs}
174
+ className={className}
175
+ leading={resolvedLeading}
176
+ leadingKey={resolvedLeadingKey}
177
+ leadingGap={leadingGap}
178
+ ready={!autoSize || isAutoSizeReady}
179
+ shadow={shadow}
149
180
  />
150
181
  ) : variant === "number" ? (
151
182
  <NumberRun
@@ -156,6 +187,10 @@ export const Laminar = React.memo(function Laminar({
156
187
  textStyle={textStyle}
157
188
  staggerMs={staggerMs}
158
189
  className={className}
190
+ leading={resolvedLeading}
191
+ leadingKey={resolvedLeadingKey}
192
+ leadingGap={leadingGap}
193
+ ready={!autoSize || isAutoSizeReady}
159
194
  />
160
195
  ) : (
161
196
  <TextRun
@@ -190,8 +225,11 @@ function isLeadingMap(
190
225
  export type {
191
226
  LaminarAlign,
192
227
  LaminarLeadingMap,
228
+ LaminarShadow,
229
+ LaminarShadowOptions,
193
230
  LaminarProps,
194
231
  MorphAnimationPresetName,
195
232
  MorphContentVariant,
233
+ MorphNumberMode,
196
234
  MorphingTextProps,
197
235
  } from "./types";
@@ -15,6 +15,7 @@ import type {
15
15
 
16
16
  // keep all presets on one curve family so layout and glyph motion agree
17
17
  const DEFAULT_CURVE = [0.19, 1, 0.22, 1] as const;
18
+ const TICKER_CURVE = [0.33, 1, 0.68, 1] as const;
18
19
 
19
20
  export const MOTION_PRESETS: Record<
20
21
  MorphAnimationPresetName,
@@ -39,6 +40,10 @@ export const MOTION_PRESETS: Record<
39
40
  duration: 0.5,
40
41
  bounce: 0.3,
41
42
  },
43
+ ticker: {
44
+ duration: 0.3,
45
+ ease: TICKER_CURVE,
46
+ },
42
47
  } as const;
43
48
 
44
49
  // the public preset table uses seconds while reanimated timing uses milliseconds
package/src/types.ts CHANGED
@@ -10,13 +10,24 @@ export type MorphAnimationPresetName =
10
10
  | "default"
11
11
  | "smooth"
12
12
  | "snappy"
13
- | "bouncy";
13
+ | "bouncy"
14
+ | "ticker";
14
15
 
15
16
  export type MorphContentVariant = "text" | "number" | "slots";
17
+ export type MorphNumberMode = "morph" | "ticker";
16
18
  export type LaminarAlign = "left" | "center" | "right";
17
19
  // text keys let leading content follow the same state as the displayed value
18
20
  export type LaminarLeadingMap = Readonly<Record<string, ReactNode>>;
19
21
 
22
+ export type LaminarShadowOptions = {
23
+ /** surface color used by the alpha mask when a platform requires it */
24
+ readonly color?: string;
25
+ /** fade depth in pixels; defaults to roughly one third of the viewport */
26
+ readonly size?: number;
27
+ };
28
+
29
+ export type LaminarShadow = boolean | LaminarShadowOptions;
30
+
20
31
  type CubicBezierTuple = readonly [number, number, number, number];
21
32
 
22
33
  export type MorphAnimationPreset =
@@ -52,16 +63,20 @@ export type MotionRecipe = {
52
63
  export type MorphingTextProps = {
53
64
  readonly text: string | number;
54
65
  readonly variant?: MorphContentVariant;
66
+ /** number-only renderer mode; ticker uses crisp in-place digit flow */
67
+ readonly numberMode?: MorphNumberMode;
55
68
  readonly fontSize?: number;
56
69
  readonly color?: string;
57
70
  readonly align?: LaminarAlign;
58
71
  readonly className?: string;
59
- /** optional inline element reconciled and animated as the leading token */
72
+ /** optional inline element reconciled and animated before the rendered value */
60
73
  readonly leading?: ReactNode | LaminarLeadingMap;
61
74
  /** identity for swapping one leading element for another */
62
75
  readonly leadingKey?: string | number;
63
76
  /** spacing between the leading element and the first text glyph */
64
77
  readonly leadingGap?: number;
78
+ /** opt-in top and bottom fades for slot reels */
79
+ readonly shadow?: LaminarShadow;
65
80
  readonly style?: StyleProp<TextStyle>;
66
81
  readonly containerStyle?: StyleProp<ViewStyle>;
67
82
  readonly fontStyle?: StyleProp<TextStyle>;
@@ -0,0 +1,45 @@
1
+ import React, { type ReactNode } from "react";
2
+ import Animated, {
3
+ type EntryExitAnimationFunction,
4
+ } from "react-native-reanimated";
5
+
6
+ type LeadingElementProps = {
7
+ readonly leading?: ReactNode;
8
+ readonly leadingKey?: string | number;
9
+ readonly leadingGap: number;
10
+ readonly enterTransition?: EntryExitAnimationFunction;
11
+ readonly elementEnterTransition?: EntryExitAnimationFunction;
12
+ readonly exitTransition?: EntryExitAnimationFunction;
13
+ readonly elementExitTransition?: EntryExitAnimationFunction;
14
+ };
15
+
16
+ // render an inline element independently so it can enter, exit, and swap beside any run
17
+ export const LeadingElement = React.memo(function LeadingElement({
18
+ leading,
19
+ leadingKey,
20
+ leadingGap,
21
+ enterTransition,
22
+ elementEnterTransition,
23
+ exitTransition,
24
+ elementExitTransition,
25
+ }: LeadingElementProps) {
26
+ if (!leading) {
27
+ return null;
28
+ }
29
+
30
+ return (
31
+ <Animated.View
32
+ key={`leading:${String(leadingKey ?? "default")}`}
33
+ entering={elementEnterTransition ?? enterTransition}
34
+ exiting={elementExitTransition ?? exitTransition}
35
+ style={[
36
+ { alignItems: "center", justifyContent: "center" },
37
+ leadingGap > 0 ? { marginRight: leadingGap } : undefined,
38
+ ]}
39
+ >
40
+ {leading}
41
+ </Animated.View>
42
+ );
43
+ });
44
+
45
+ LeadingElement.displayName = "LeadingElement";
@@ -1,7 +1,8 @@
1
- import React, { useMemo } from "react";
1
+ import React, { useMemo, useRef } from "react";
2
2
  import type { LayoutChangeEvent, StyleProp, ViewStyle } from "react-native";
3
3
  import { View } from "react-native";
4
4
  import Animated from "react-native-reanimated";
5
+ import type { ComplexAnimationBuilder } from "react-native-reanimated";
5
6
  import type { LaminarAlign } from "../types";
6
7
 
7
8
  const shellStyle = {
@@ -52,6 +53,8 @@ type MorphViewportProps = {
52
53
  readonly align: LaminarAlign;
53
54
  readonly containerStyle?: StyleProp<ViewStyle>;
54
55
  readonly animatedWidthStyle?: React.ComponentProps<typeof Animated.View>["style"];
56
+ /** optional layout-driven auto-size path for content that needs an aligned anchor */
57
+ readonly autoSizeLayoutTransition?: ComplexAnimationBuilder;
55
58
  readonly onVisibleLayout?: (event: LayoutChangeEvent) => void;
56
59
  readonly measurement?: React.ReactNode;
57
60
  readonly children: React.ReactNode;
@@ -65,10 +68,16 @@ export const MorphViewport = React.memo(
65
68
  align,
66
69
  containerStyle,
67
70
  animatedWidthStyle,
71
+ autoSizeLayoutTransition,
68
72
  onVisibleLayout,
69
73
  measurement,
70
74
  children,
71
75
  }: MorphViewportProps) => {
76
+ const hasRenderedRef = useRef(false);
77
+ const shouldAnimateAutoSizeLayout =
78
+ autoSizeLayoutTransition !== undefined && hasRenderedRef.current;
79
+ hasRenderedRef.current = true;
80
+
72
81
  // compute alignment once so the animated shell and its child share one layout contract
73
82
  const resolvedViewportStyle = useMemo(
74
83
  () => [
@@ -80,6 +89,35 @@ export const MorphViewport = React.memo(
80
89
  );
81
90
 
82
91
  // keep measurement outside the visible viewport so it cannot affect accessibility or layout
92
+ if (autoSizeLayoutTransition) {
93
+ // Layout animation owns both width and the shell's aligned x-position.
94
+ // This is important for center/right auto-size: animating only a child
95
+ // width leaves the parent anchor at its old position.
96
+ return (
97
+ <Animated.View
98
+ layout={
99
+ shouldAnimateAutoSizeLayout
100
+ ? autoSizeLayoutTransition
101
+ : undefined
102
+ }
103
+ style={[shellStyle, shellAlignStyles[align], containerStyle]}
104
+ >
105
+ <View
106
+ accessibilityElementsHidden
107
+ collapsable={false}
108
+ importantForAccessibility="no-hide-descendants"
109
+ pointerEvents="none"
110
+ style={measuredContentStyle}
111
+ >
112
+ {measurement}
113
+ </View>
114
+ <Animated.View onLayout={onVisibleLayout} style={resolvedViewportStyle}>
115
+ {children}
116
+ </Animated.View>
117
+ </Animated.View>
118
+ );
119
+ }
120
+
83
121
  return (
84
122
  <View style={[shellStyle, shellAlignStyles[align], containerStyle]}>
85
123
  {autoSize ? (