@hero-design/rn 8.140.0 → 8.141.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @hero-design/rn
2
2
 
3
+ ## 8.141.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#5569](https://github.com/Thinkei/hero-design/pull/5569) [`08e26bb`](https://github.com/Thinkei/hero-design/commit/08e26bb64cb2de40cda008482afc9992e5de1e29) Thanks [@dathuynh-eh](https://github.com/dathuynh-eh)! - [Confetti] Add Confetti component with ConfettiProvider and useConfetti hook for celebratory burst animations
8
+
3
9
  ## 8.140.0
4
10
 
5
11
  ### Minor Changes
package/es/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as reactNative from 'react-native';
2
- import { StyleSheet as StyleSheet$1, Platform, Dimensions, Keyboard, Animated, View, UIManager, LayoutAnimation, TouchableOpacity, Text as Text$1, Easing, useWindowDimensions, TouchableWithoutFeedback, Modal as Modal$1, Image as Image$1, Pressable, KeyboardAvoidingView, TouchableHighlight, ScrollView, FlatList, TextInput as TextInput$1, PanResponder, BackHandler, InteractionManager, SectionList as SectionList$1, PixelRatio, RefreshControl as RefreshControl$1 } from 'react-native';
2
+ import { StyleSheet as StyleSheet$1, Platform, Dimensions, Keyboard, Animated, View, UIManager, LayoutAnimation, TouchableOpacity, Text as Text$1, Easing, useWindowDimensions, AccessibilityInfo, TouchableWithoutFeedback, Modal as Modal$1, Image as Image$1, Pressable, KeyboardAvoidingView, TouchableHighlight, ScrollView, FlatList, TextInput as TextInput$1, PanResponder, BackHandler, InteractionManager, SectionList as SectionList$1, PixelRatio, RefreshControl as RefreshControl$1 } from 'react-native';
3
3
  import * as React from 'react';
4
- import React__default, { useState, useEffect, useMemo, useCallback, useRef, useLayoutEffect, createContext, forwardRef, useContext, memo, useReducer, isValidElement, useImperativeHandle } from 'react';
4
+ import React__default, { useState, useEffect, useMemo, useCallback, useRef, createContext, useContext, useLayoutEffect, forwardRef, memo, useReducer, isValidElement, useImperativeHandle } from 'react';
5
5
  import MaskedView from '@react-native-masked-view/masked-view';
6
6
  import { LinearGradient } from 'expo-linear-gradient';
7
7
  import { createIconSet } from 'react-native-vector-icons';
@@ -3324,7 +3324,7 @@ function interleave(vals) {
3324
3324
  // this is done so we don't create a new
3325
3325
  // handleInterpolation function on every css call
3326
3326
 
3327
- var styles;
3327
+ var styles$1;
3328
3328
  var generated = {};
3329
3329
  var buffer = '';
3330
3330
  var lastType;
@@ -3347,7 +3347,7 @@ function handleInterpolation(interpolation, i, arr) {
3347
3347
  if (lastType === 'string' && (isRnStyle || isIrrelevant)) {
3348
3348
  var converted = convertStyles(buffer);
3349
3349
  if (converted !== undefined) {
3350
- styles.push(converted);
3350
+ styles$1.push(converted);
3351
3351
  }
3352
3352
  buffer = '';
3353
3353
  }
@@ -3359,13 +3359,13 @@ function handleInterpolation(interpolation, i, arr) {
3359
3359
  if (arr.length - 1 === i) {
3360
3360
  var _converted = convertStyles(buffer);
3361
3361
  if (_converted !== undefined) {
3362
- styles.push(_converted);
3362
+ styles$1.push(_converted);
3363
3363
  }
3364
3364
  buffer = '';
3365
3365
  }
3366
3366
  }
3367
3367
  if (isRnStyle) {
3368
- styles.push(interpolation);
3368
+ styles$1.push(interpolation);
3369
3369
  }
3370
3370
  if (Array.isArray(interpolation)) {
3371
3371
  interpolation.forEach(handleInterpolation, this);
@@ -3381,7 +3381,7 @@ function createCss(StyleSheet) {
3381
3381
  // this is done so we don't create a new
3382
3382
  // handleInterpolation function on every css call
3383
3383
 
3384
- styles = [];
3384
+ styles$1 = [];
3385
3385
  buffer = '';
3386
3386
  lastType = undefined;
3387
3387
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
@@ -3397,10 +3397,10 @@ function createCss(StyleSheet) {
3397
3397
  } finally {
3398
3398
  buffer = prevBuffer;
3399
3399
  }
3400
- var hash = JSON.stringify(styles);
3400
+ var hash = JSON.stringify(styles$1);
3401
3401
  if (!generated[hash]) {
3402
3402
  var styleSheet = StyleSheet.create({
3403
- generated: StyleSheet.flatten(styles)
3403
+ generated: StyleSheet.flatten(styles$1)
3404
3404
  });
3405
3405
  generated[hash] = styleSheet.generated;
3406
3406
  }
@@ -9387,6 +9387,274 @@ var Accordion = function Accordion(_ref) {
9387
9387
  }));
9388
9388
  };
9389
9389
 
9390
+ function reflectX(rawX, width) {
9391
+ var period = 2 * width;
9392
+ var modX = (rawX % period + period) % period;
9393
+ return modX < width ? modX : period - modX;
9394
+ }
9395
+ /**
9396
+ * Pre-computes the reflectX triangle-wave as interpolation keyframes.
9397
+ * Run once in JS before animation starts; native driver handles per-frame lerp.
9398
+ */
9399
+ function computeXKeyframes(initialX, xVel, durationSeconds, width) {
9400
+ if (xVel === 0) {
9401
+ return {
9402
+ inputRange: [0, 1],
9403
+ outputRange: [initialX, initialX]
9404
+ };
9405
+ }
9406
+ var rawEnd = initialX + xVel * durationSeconds;
9407
+ var rawMin = Math.min(initialX, rawEnd);
9408
+ var rawMax = Math.max(initialX, rawEnd);
9409
+ var nMin = Math.ceil(rawMin / width);
9410
+ var nMax = Math.floor(rawMax / width);
9411
+ var times = [0];
9412
+ for (var n = nMin; n <= nMax; n += 1) {
9413
+ var t = (n * width - initialX) / xVel;
9414
+ if (t > 0 && t < durationSeconds) times.push(t);
9415
+ }
9416
+ times.push(durationSeconds);
9417
+ times.sort(function (a, b) {
9418
+ return a - b;
9419
+ });
9420
+ return {
9421
+ inputRange: times.map(function (t) {
9422
+ return t / durationSeconds;
9423
+ }),
9424
+ outputRange: times.map(function (t) {
9425
+ return reflectX(initialX + xVel * t, width);
9426
+ })
9427
+ };
9428
+ }
9429
+
9430
+ var CONFETTI_MIN_SIZE = 4;
9431
+ var CONFETTI_MAX_SIZE = 20;
9432
+ var BATCH_SIZE = 10;
9433
+ var BATCH_DELAY_MS = 300;
9434
+ var FADE_OUT_MS = 500;
9435
+ var Y_START_OFFSET = -60;
9436
+ var styles = StyleSheet$1.create({
9437
+ container: _objectSpread2(_objectSpread2({}, StyleSheet$1.absoluteFillObject), {}, {
9438
+ overflow: 'hidden'
9439
+ }),
9440
+ piece: {
9441
+ position: 'absolute',
9442
+ left: 0,
9443
+ top: 0
9444
+ }
9445
+ });
9446
+ function randomBetween(min, max) {
9447
+ return Math.random() * (max - min) + min;
9448
+ }
9449
+ var ConfettiPiece = function ConfettiPiece(_ref) {
9450
+ var angleVel = _ref.angleVel,
9451
+ color = _ref.color,
9452
+ containerWidth = _ref.containerWidth,
9453
+ delay = _ref.delay,
9454
+ duration = _ref.duration,
9455
+ initialX = _ref.initialX,
9456
+ onFinish = _ref.onFinish,
9457
+ size = _ref.size,
9458
+ xVel = _ref.xVel,
9459
+ yVel = _ref.yVel;
9460
+ var progress = useRef(new Animated.Value(0)).current;
9461
+ var durationSeconds = duration / 1000;
9462
+ var xKeyframes = useMemo(function () {
9463
+ return computeXKeyframes(initialX, xVel, durationSeconds, containerWidth);
9464
+ },
9465
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9466
+ []);
9467
+ var translateX = progress.interpolate({
9468
+ inputRange: xKeyframes.inputRange,
9469
+ outputRange: xKeyframes.outputRange
9470
+ });
9471
+ var translateY = progress.interpolate({
9472
+ inputRange: [0, 1],
9473
+ outputRange: [Y_START_OFFSET, Y_START_OFFSET + yVel * durationSeconds]
9474
+ });
9475
+ var rotate = progress.interpolate({
9476
+ inputRange: [0, 1],
9477
+ outputRange: ['0rad', "".concat(angleVel * durationSeconds, "rad")]
9478
+ });
9479
+ useEffect(function () {
9480
+ Animated.sequence([Animated.delay(delay), Animated.timing(progress, {
9481
+ toValue: 1,
9482
+ duration: duration,
9483
+ useNativeDriver: true
9484
+ })]).start(function (_ref2) {
9485
+ var finished = _ref2.finished;
9486
+ if (finished) onFinish();
9487
+ });
9488
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9489
+ }, []);
9490
+ return /*#__PURE__*/React__default.createElement(Animated.View, {
9491
+ style: [styles.piece, {
9492
+ width: size,
9493
+ height: size / 2,
9494
+ backgroundColor: color
9495
+ }, {
9496
+ transform: [{
9497
+ translateX: translateX
9498
+ }, {
9499
+ translateY: translateY
9500
+ }, {
9501
+ rotate: rotate
9502
+ }]
9503
+ }]
9504
+ });
9505
+ };
9506
+ var Confetti$2 = function Confetti(_ref3) {
9507
+ var _pieces$current;
9508
+ var colorsProp = _ref3.colors,
9509
+ _ref3$numberOfPieces = _ref3.numberOfPieces,
9510
+ numberOfPieces = _ref3$numberOfPieces === void 0 ? 80 : _ref3$numberOfPieces,
9511
+ _ref3$duration = _ref3.duration,
9512
+ duration = _ref3$duration === void 0 ? 3000 : _ref3$duration,
9513
+ onFinish = _ref3.onFinish,
9514
+ run = _ref3.run,
9515
+ testID = _ref3.testID;
9516
+ var theme = useTheme();
9517
+ var _useWindowDimensions = useWindowDimensions(),
9518
+ screenHeight = _useWindowDimensions.height;
9519
+ var _useState = useState(false),
9520
+ _useState2 = _slicedToArray(_useState, 2),
9521
+ isReducedMotion = _useState2[0],
9522
+ setIsReducedMotion = _useState2[1];
9523
+ var _useState3 = useState(false),
9524
+ _useState4 = _slicedToArray(_useState3, 2),
9525
+ isVisible = _useState4[0],
9526
+ setIsVisible = _useState4[1];
9527
+ var _useState5 = useState(0),
9528
+ _useState6 = _slicedToArray(_useState5, 2),
9529
+ containerWidth = _useState6[0],
9530
+ setContainerWidth = _useState6[1];
9531
+ var containerOpacity = useRef(new Animated.Value(0)).current;
9532
+ var finishedCount = useRef(0);
9533
+ var pieces = useRef(null);
9534
+ useEffect(function () {
9535
+ AccessibilityInfo.isReduceMotionEnabled().then(setIsReducedMotion);
9536
+ var sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setIsReducedMotion);
9537
+ return function () {
9538
+ return sub.remove();
9539
+ };
9540
+ }, []);
9541
+ useEffect(function () {
9542
+ if (run && !isReducedMotion && containerWidth > 0) {
9543
+ var durationSeconds = duration / 1000;
9544
+ var yVelMin = (screenHeight + 80) / durationSeconds;
9545
+ var yVelMax = yVelMin * 1.5;
9546
+ var colors = colorsProp !== null && colorsProp !== void 0 ? colorsProp : [theme.colors.primary, theme.colors.secondary];
9547
+ pieces.current = Array.from({
9548
+ length: numberOfPieces
9549
+ }, function (_, i) {
9550
+ return {
9551
+ color: colors[i % colors.length],
9552
+ size: Math.round(randomBetween(CONFETTI_MIN_SIZE, CONFETTI_MAX_SIZE)),
9553
+ initialX: randomBetween(containerWidth * 0.2, containerWidth * 0.8),
9554
+ xVel: randomBetween(-200, 200),
9555
+ yVel: randomBetween(yVelMin, yVelMax),
9556
+ angleVel: randomBetween(-Math.PI * 1.5, Math.PI * 1.5),
9557
+ delay: Math.floor(i / BATCH_SIZE) * BATCH_DELAY_MS
9558
+ };
9559
+ });
9560
+ finishedCount.current = 0;
9561
+ containerOpacity.setValue(1);
9562
+ setIsVisible(true);
9563
+ }
9564
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9565
+ }, [run, containerWidth]);
9566
+ var handlePieceFinish = useCallback(function () {
9567
+ finishedCount.current += 1;
9568
+ if (finishedCount.current >= numberOfPieces) {
9569
+ Animated.timing(containerOpacity, {
9570
+ toValue: 0,
9571
+ duration: FADE_OUT_MS,
9572
+ useNativeDriver: true
9573
+ }).start(function (_ref4) {
9574
+ var finished = _ref4.finished;
9575
+ if (finished) {
9576
+ setIsVisible(false);
9577
+ onFinish === null || onFinish === void 0 || onFinish();
9578
+ }
9579
+ });
9580
+ }
9581
+ }, [containerOpacity, numberOfPieces, onFinish]);
9582
+ var handleLayout = useCallback(function (_ref5) {
9583
+ var nativeEvent = _ref5.nativeEvent;
9584
+ var width = nativeEvent.layout.width;
9585
+ if (width > 0) setContainerWidth(width);
9586
+ }, []);
9587
+ if (isReducedMotion) {
9588
+ return null;
9589
+ }
9590
+ // Always render the container (when not in reduced motion) so onLayout fires and we know the real width.
9591
+ // Visibility is controlled via opacity to avoid losing the measurement.
9592
+ return /*#__PURE__*/React__default.createElement(Animated.View, {
9593
+ pointerEvents: "none",
9594
+ onLayout: handleLayout,
9595
+ style: [styles.container, {
9596
+ opacity: isVisible ? containerOpacity : 0
9597
+ }],
9598
+ testID: isVisible ? testID : undefined
9599
+ }, isVisible && ((_pieces$current = pieces.current) === null || _pieces$current === void 0 ? void 0 : _pieces$current.map(function (piece, id) {
9600
+ return /*#__PURE__*/React__default.createElement(ConfettiPiece, _extends$1({
9601
+ key: id
9602
+ }, piece, {
9603
+ containerWidth: containerWidth,
9604
+ duration: duration,
9605
+ onFinish: handlePieceFinish
9606
+ }));
9607
+ })));
9608
+ };
9609
+
9610
+ var ConfettiContext = /*#__PURE__*/createContext(null);
9611
+ var ConfettiProvider = function ConfettiProvider(_ref) {
9612
+ var children = _ref.children;
9613
+ var _useState = useState(false),
9614
+ _useState2 = _slicedToArray(_useState, 2),
9615
+ run = _useState2[0],
9616
+ setRun = _useState2[1];
9617
+ var _useState3 = useState(),
9618
+ _useState4 = _slicedToArray(_useState3, 2),
9619
+ callConfig = _useState4[0],
9620
+ setCallConfig = _useState4[1];
9621
+ var onFinishedRef = useRef(undefined);
9622
+ var showConfetti = useCallback(function (config, onFinished) {
9623
+ setCallConfig(config);
9624
+ onFinishedRef.current = onFinished;
9625
+ setRun(true);
9626
+ }, []);
9627
+ var handleFinish = useCallback(function () {
9628
+ var _onFinishedRef$curren;
9629
+ setRun(false);
9630
+ (_onFinishedRef$curren = onFinishedRef.current) === null || _onFinishedRef$curren === void 0 || _onFinishedRef$curren.call(onFinishedRef, true);
9631
+ }, []);
9632
+ var contextValue = useMemo(function () {
9633
+ return {
9634
+ showConfetti: showConfetti
9635
+ };
9636
+ }, [showConfetti]);
9637
+ return /*#__PURE__*/React__default.createElement(ConfettiContext.Provider, {
9638
+ value: contextValue
9639
+ }, children, /*#__PURE__*/React__default.createElement(Confetti$2, _extends$1({
9640
+ run: run,
9641
+ onFinish: handleFinish
9642
+ }, callConfig)));
9643
+ };
9644
+
9645
+ var useConfetti = function useConfetti() {
9646
+ var context = useContext(ConfettiContext);
9647
+ if (!context) {
9648
+ throw new Error('useConfetti must be used within a Confetti.Provider');
9649
+ }
9650
+ return context;
9651
+ };
9652
+
9653
+ var Confetti$1 = Object.assign(Confetti$2, {
9654
+ Provider: ConfettiProvider,
9655
+ useConfetti: useConfetti
9656
+ });
9657
+
9390
9658
  var Container$1 = index$c(View)(function (_ref) {
9391
9659
  var theme = _ref.theme,
9392
9660
  _ref$themeVariant = _ref.themeVariant,
@@ -23108,6 +23376,7 @@ var Confetti = function Confetti(_ref) {
23108
23376
  strokeWidth: "0.4"
23109
23377
  })));
23110
23378
  };
23379
+ Confetti.displayName = 'ConfettiIllustration';
23111
23380
 
23112
23381
  var Connections = function Connections(_ref) {
23113
23382
  var stroke = _ref.stroke,
@@ -43577,4 +43846,4 @@ var InlineLoader = function InlineLoader(_ref) {
43577
43846
  }, text));
43578
43847
  };
43579
43848
 
43580
- export { Accordion, Alert, AppCue, Attachment, index$b as Avatar, Badge, BottomNavigation, BottomSheet, Box, CompoundButton as Button, Calendar, Card, index$a as Carousel, Chart, index$9 as Checkbox, Chip, Collapse, ContentNavigator, index$8 as DatePicker, Divider, index$7 as Drawer, Empty, ErrorComponent as Error, FAB, FilterTrigger, FlatListWithFAB, FloatingIsland, HeroDesignProvider, Icon, Illustration, IllustrationList, Image, InlineLoader, List$1 as List, LocaleProvider, index$6 as MapPin, PageControl, PinInput, Portal, Progress, CompoundRadio as Radio, Rate, RefreshControl, index as RichTextEditor, ScrollViewWithFAB, Search, SectionHeading, SectionList, SectionListWithFAB, SegmentedControl, index$4 as Select, Skeleton, Slider, Spinner, Success, index$5 as Swipeable, index$3 as Switch, index$2 as Tabs, Tag, TextInput, ThemeProvider, ThemeSwitcher, PublicTimePicker as TimePicker, Toast, index$1 as Toolbar, Typography, eBensSystemPalette, ehJobsShadowPalette, ehJobsSystemPalette, ehWorkDarkShadowPalette, ehWorkDarkSystemPalette, ehWorkShadowPalette, ehWorkSystemPalette, getTheme, jobsSystemPalette, scale, index$c as styled, swagDarkSystemPalette, swagLightJobsSystemPalette, swagSystemPalette$1 as swagLightSystemPalette, swagSystemPalette$2 as swagSystemPalette, defaultTheme as theme, useAvatarColors, useTheme, walletSystemPalette, withTheme, workSystemPalette };
43849
+ export { Accordion, Alert, AppCue, Attachment, index$b as Avatar, Badge, BottomNavigation, BottomSheet, Box, CompoundButton as Button, Calendar, Card, index$a as Carousel, Chart, index$9 as Checkbox, Chip, Collapse, Confetti$1 as Confetti, ContentNavigator, index$8 as DatePicker, Divider, index$7 as Drawer, Empty, ErrorComponent as Error, FAB, FilterTrigger, FlatListWithFAB, FloatingIsland, HeroDesignProvider, Icon, Illustration, IllustrationList, Image, InlineLoader, List$1 as List, LocaleProvider, index$6 as MapPin, PageControl, PinInput, Portal, Progress, CompoundRadio as Radio, Rate, RefreshControl, index as RichTextEditor, ScrollViewWithFAB, Search, SectionHeading, SectionList, SectionListWithFAB, SegmentedControl, index$4 as Select, Skeleton, Slider, Spinner, Success, index$5 as Swipeable, index$3 as Switch, index$2 as Tabs, Tag, TextInput, ThemeProvider, ThemeSwitcher, PublicTimePicker as TimePicker, Toast, index$1 as Toolbar, Typography, eBensSystemPalette, ehJobsShadowPalette, ehJobsSystemPalette, ehWorkDarkShadowPalette, ehWorkDarkSystemPalette, ehWorkShadowPalette, ehWorkSystemPalette, getTheme, jobsSystemPalette, scale, index$c as styled, swagDarkSystemPalette, swagLightJobsSystemPalette, swagSystemPalette$1 as swagLightSystemPalette, swagSystemPalette$2 as swagSystemPalette, defaultTheme as theme, useAvatarColors, useTheme, walletSystemPalette, withTheme, workSystemPalette };
package/lib/index.js CHANGED
@@ -3353,7 +3353,7 @@ function interleave(vals) {
3353
3353
  // this is done so we don't create a new
3354
3354
  // handleInterpolation function on every css call
3355
3355
 
3356
- var styles;
3356
+ var styles$1;
3357
3357
  var generated = {};
3358
3358
  var buffer = '';
3359
3359
  var lastType;
@@ -3376,7 +3376,7 @@ function handleInterpolation(interpolation, i, arr) {
3376
3376
  if (lastType === 'string' && (isRnStyle || isIrrelevant)) {
3377
3377
  var converted = convertStyles(buffer);
3378
3378
  if (converted !== undefined) {
3379
- styles.push(converted);
3379
+ styles$1.push(converted);
3380
3380
  }
3381
3381
  buffer = '';
3382
3382
  }
@@ -3388,13 +3388,13 @@ function handleInterpolation(interpolation, i, arr) {
3388
3388
  if (arr.length - 1 === i) {
3389
3389
  var _converted = convertStyles(buffer);
3390
3390
  if (_converted !== undefined) {
3391
- styles.push(_converted);
3391
+ styles$1.push(_converted);
3392
3392
  }
3393
3393
  buffer = '';
3394
3394
  }
3395
3395
  }
3396
3396
  if (isRnStyle) {
3397
- styles.push(interpolation);
3397
+ styles$1.push(interpolation);
3398
3398
  }
3399
3399
  if (Array.isArray(interpolation)) {
3400
3400
  interpolation.forEach(handleInterpolation, this);
@@ -3410,7 +3410,7 @@ function createCss(StyleSheet) {
3410
3410
  // this is done so we don't create a new
3411
3411
  // handleInterpolation function on every css call
3412
3412
 
3413
- styles = [];
3413
+ styles$1 = [];
3414
3414
  buffer = '';
3415
3415
  lastType = undefined;
3416
3416
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
@@ -3426,10 +3426,10 @@ function createCss(StyleSheet) {
3426
3426
  } finally {
3427
3427
  buffer = prevBuffer;
3428
3428
  }
3429
- var hash = JSON.stringify(styles);
3429
+ var hash = JSON.stringify(styles$1);
3430
3430
  if (!generated[hash]) {
3431
3431
  var styleSheet = StyleSheet.create({
3432
- generated: StyleSheet.flatten(styles)
3432
+ generated: StyleSheet.flatten(styles$1)
3433
3433
  });
3434
3434
  generated[hash] = styleSheet.generated;
3435
3435
  }
@@ -9416,6 +9416,274 @@ var Accordion = function Accordion(_ref) {
9416
9416
  }));
9417
9417
  };
9418
9418
 
9419
+ function reflectX(rawX, width) {
9420
+ var period = 2 * width;
9421
+ var modX = (rawX % period + period) % period;
9422
+ return modX < width ? modX : period - modX;
9423
+ }
9424
+ /**
9425
+ * Pre-computes the reflectX triangle-wave as interpolation keyframes.
9426
+ * Run once in JS before animation starts; native driver handles per-frame lerp.
9427
+ */
9428
+ function computeXKeyframes(initialX, xVel, durationSeconds, width) {
9429
+ if (xVel === 0) {
9430
+ return {
9431
+ inputRange: [0, 1],
9432
+ outputRange: [initialX, initialX]
9433
+ };
9434
+ }
9435
+ var rawEnd = initialX + xVel * durationSeconds;
9436
+ var rawMin = Math.min(initialX, rawEnd);
9437
+ var rawMax = Math.max(initialX, rawEnd);
9438
+ var nMin = Math.ceil(rawMin / width);
9439
+ var nMax = Math.floor(rawMax / width);
9440
+ var times = [0];
9441
+ for (var n = nMin; n <= nMax; n += 1) {
9442
+ var t = (n * width - initialX) / xVel;
9443
+ if (t > 0 && t < durationSeconds) times.push(t);
9444
+ }
9445
+ times.push(durationSeconds);
9446
+ times.sort(function (a, b) {
9447
+ return a - b;
9448
+ });
9449
+ return {
9450
+ inputRange: times.map(function (t) {
9451
+ return t / durationSeconds;
9452
+ }),
9453
+ outputRange: times.map(function (t) {
9454
+ return reflectX(initialX + xVel * t, width);
9455
+ })
9456
+ };
9457
+ }
9458
+
9459
+ var CONFETTI_MIN_SIZE = 4;
9460
+ var CONFETTI_MAX_SIZE = 20;
9461
+ var BATCH_SIZE = 10;
9462
+ var BATCH_DELAY_MS = 300;
9463
+ var FADE_OUT_MS = 500;
9464
+ var Y_START_OFFSET = -60;
9465
+ var styles = reactNative.StyleSheet.create({
9466
+ container: _objectSpread2(_objectSpread2({}, reactNative.StyleSheet.absoluteFillObject), {}, {
9467
+ overflow: 'hidden'
9468
+ }),
9469
+ piece: {
9470
+ position: 'absolute',
9471
+ left: 0,
9472
+ top: 0
9473
+ }
9474
+ });
9475
+ function randomBetween(min, max) {
9476
+ return Math.random() * (max - min) + min;
9477
+ }
9478
+ var ConfettiPiece = function ConfettiPiece(_ref) {
9479
+ var angleVel = _ref.angleVel,
9480
+ color = _ref.color,
9481
+ containerWidth = _ref.containerWidth,
9482
+ delay = _ref.delay,
9483
+ duration = _ref.duration,
9484
+ initialX = _ref.initialX,
9485
+ onFinish = _ref.onFinish,
9486
+ size = _ref.size,
9487
+ xVel = _ref.xVel,
9488
+ yVel = _ref.yVel;
9489
+ var progress = React.useRef(new reactNative.Animated.Value(0)).current;
9490
+ var durationSeconds = duration / 1000;
9491
+ var xKeyframes = React.useMemo(function () {
9492
+ return computeXKeyframes(initialX, xVel, durationSeconds, containerWidth);
9493
+ },
9494
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9495
+ []);
9496
+ var translateX = progress.interpolate({
9497
+ inputRange: xKeyframes.inputRange,
9498
+ outputRange: xKeyframes.outputRange
9499
+ });
9500
+ var translateY = progress.interpolate({
9501
+ inputRange: [0, 1],
9502
+ outputRange: [Y_START_OFFSET, Y_START_OFFSET + yVel * durationSeconds]
9503
+ });
9504
+ var rotate = progress.interpolate({
9505
+ inputRange: [0, 1],
9506
+ outputRange: ['0rad', "".concat(angleVel * durationSeconds, "rad")]
9507
+ });
9508
+ React.useEffect(function () {
9509
+ reactNative.Animated.sequence([reactNative.Animated.delay(delay), reactNative.Animated.timing(progress, {
9510
+ toValue: 1,
9511
+ duration: duration,
9512
+ useNativeDriver: true
9513
+ })]).start(function (_ref2) {
9514
+ var finished = _ref2.finished;
9515
+ if (finished) onFinish();
9516
+ });
9517
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9518
+ }, []);
9519
+ return /*#__PURE__*/React__namespace.default.createElement(reactNative.Animated.View, {
9520
+ style: [styles.piece, {
9521
+ width: size,
9522
+ height: size / 2,
9523
+ backgroundColor: color
9524
+ }, {
9525
+ transform: [{
9526
+ translateX: translateX
9527
+ }, {
9528
+ translateY: translateY
9529
+ }, {
9530
+ rotate: rotate
9531
+ }]
9532
+ }]
9533
+ });
9534
+ };
9535
+ var Confetti$2 = function Confetti(_ref3) {
9536
+ var _pieces$current;
9537
+ var colorsProp = _ref3.colors,
9538
+ _ref3$numberOfPieces = _ref3.numberOfPieces,
9539
+ numberOfPieces = _ref3$numberOfPieces === void 0 ? 80 : _ref3$numberOfPieces,
9540
+ _ref3$duration = _ref3.duration,
9541
+ duration = _ref3$duration === void 0 ? 3000 : _ref3$duration,
9542
+ onFinish = _ref3.onFinish,
9543
+ run = _ref3.run,
9544
+ testID = _ref3.testID;
9545
+ var theme = useTheme();
9546
+ var _useWindowDimensions = reactNative.useWindowDimensions(),
9547
+ screenHeight = _useWindowDimensions.height;
9548
+ var _useState = React.useState(false),
9549
+ _useState2 = _slicedToArray(_useState, 2),
9550
+ isReducedMotion = _useState2[0],
9551
+ setIsReducedMotion = _useState2[1];
9552
+ var _useState3 = React.useState(false),
9553
+ _useState4 = _slicedToArray(_useState3, 2),
9554
+ isVisible = _useState4[0],
9555
+ setIsVisible = _useState4[1];
9556
+ var _useState5 = React.useState(0),
9557
+ _useState6 = _slicedToArray(_useState5, 2),
9558
+ containerWidth = _useState6[0],
9559
+ setContainerWidth = _useState6[1];
9560
+ var containerOpacity = React.useRef(new reactNative.Animated.Value(0)).current;
9561
+ var finishedCount = React.useRef(0);
9562
+ var pieces = React.useRef(null);
9563
+ React.useEffect(function () {
9564
+ reactNative.AccessibilityInfo.isReduceMotionEnabled().then(setIsReducedMotion);
9565
+ var sub = reactNative.AccessibilityInfo.addEventListener('reduceMotionChanged', setIsReducedMotion);
9566
+ return function () {
9567
+ return sub.remove();
9568
+ };
9569
+ }, []);
9570
+ React.useEffect(function () {
9571
+ if (run && !isReducedMotion && containerWidth > 0) {
9572
+ var durationSeconds = duration / 1000;
9573
+ var yVelMin = (screenHeight + 80) / durationSeconds;
9574
+ var yVelMax = yVelMin * 1.5;
9575
+ var colors = colorsProp !== null && colorsProp !== void 0 ? colorsProp : [theme.colors.primary, theme.colors.secondary];
9576
+ pieces.current = Array.from({
9577
+ length: numberOfPieces
9578
+ }, function (_, i) {
9579
+ return {
9580
+ color: colors[i % colors.length],
9581
+ size: Math.round(randomBetween(CONFETTI_MIN_SIZE, CONFETTI_MAX_SIZE)),
9582
+ initialX: randomBetween(containerWidth * 0.2, containerWidth * 0.8),
9583
+ xVel: randomBetween(-200, 200),
9584
+ yVel: randomBetween(yVelMin, yVelMax),
9585
+ angleVel: randomBetween(-Math.PI * 1.5, Math.PI * 1.5),
9586
+ delay: Math.floor(i / BATCH_SIZE) * BATCH_DELAY_MS
9587
+ };
9588
+ });
9589
+ finishedCount.current = 0;
9590
+ containerOpacity.setValue(1);
9591
+ setIsVisible(true);
9592
+ }
9593
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9594
+ }, [run, containerWidth]);
9595
+ var handlePieceFinish = React.useCallback(function () {
9596
+ finishedCount.current += 1;
9597
+ if (finishedCount.current >= numberOfPieces) {
9598
+ reactNative.Animated.timing(containerOpacity, {
9599
+ toValue: 0,
9600
+ duration: FADE_OUT_MS,
9601
+ useNativeDriver: true
9602
+ }).start(function (_ref4) {
9603
+ var finished = _ref4.finished;
9604
+ if (finished) {
9605
+ setIsVisible(false);
9606
+ onFinish === null || onFinish === void 0 || onFinish();
9607
+ }
9608
+ });
9609
+ }
9610
+ }, [containerOpacity, numberOfPieces, onFinish]);
9611
+ var handleLayout = React.useCallback(function (_ref5) {
9612
+ var nativeEvent = _ref5.nativeEvent;
9613
+ var width = nativeEvent.layout.width;
9614
+ if (width > 0) setContainerWidth(width);
9615
+ }, []);
9616
+ if (isReducedMotion) {
9617
+ return null;
9618
+ }
9619
+ // Always render the container (when not in reduced motion) so onLayout fires and we know the real width.
9620
+ // Visibility is controlled via opacity to avoid losing the measurement.
9621
+ return /*#__PURE__*/React__namespace.default.createElement(reactNative.Animated.View, {
9622
+ pointerEvents: "none",
9623
+ onLayout: handleLayout,
9624
+ style: [styles.container, {
9625
+ opacity: isVisible ? containerOpacity : 0
9626
+ }],
9627
+ testID: isVisible ? testID : undefined
9628
+ }, isVisible && ((_pieces$current = pieces.current) === null || _pieces$current === void 0 ? void 0 : _pieces$current.map(function (piece, id) {
9629
+ return /*#__PURE__*/React__namespace.default.createElement(ConfettiPiece, _extends$1({
9630
+ key: id
9631
+ }, piece, {
9632
+ containerWidth: containerWidth,
9633
+ duration: duration,
9634
+ onFinish: handlePieceFinish
9635
+ }));
9636
+ })));
9637
+ };
9638
+
9639
+ var ConfettiContext = /*#__PURE__*/React.createContext(null);
9640
+ var ConfettiProvider = function ConfettiProvider(_ref) {
9641
+ var children = _ref.children;
9642
+ var _useState = React.useState(false),
9643
+ _useState2 = _slicedToArray(_useState, 2),
9644
+ run = _useState2[0],
9645
+ setRun = _useState2[1];
9646
+ var _useState3 = React.useState(),
9647
+ _useState4 = _slicedToArray(_useState3, 2),
9648
+ callConfig = _useState4[0],
9649
+ setCallConfig = _useState4[1];
9650
+ var onFinishedRef = React.useRef(undefined);
9651
+ var showConfetti = React.useCallback(function (config, onFinished) {
9652
+ setCallConfig(config);
9653
+ onFinishedRef.current = onFinished;
9654
+ setRun(true);
9655
+ }, []);
9656
+ var handleFinish = React.useCallback(function () {
9657
+ var _onFinishedRef$curren;
9658
+ setRun(false);
9659
+ (_onFinishedRef$curren = onFinishedRef.current) === null || _onFinishedRef$curren === void 0 || _onFinishedRef$curren.call(onFinishedRef, true);
9660
+ }, []);
9661
+ var contextValue = React.useMemo(function () {
9662
+ return {
9663
+ showConfetti: showConfetti
9664
+ };
9665
+ }, [showConfetti]);
9666
+ return /*#__PURE__*/React__namespace.default.createElement(ConfettiContext.Provider, {
9667
+ value: contextValue
9668
+ }, children, /*#__PURE__*/React__namespace.default.createElement(Confetti$2, _extends$1({
9669
+ run: run,
9670
+ onFinish: handleFinish
9671
+ }, callConfig)));
9672
+ };
9673
+
9674
+ var useConfetti = function useConfetti() {
9675
+ var context = React.useContext(ConfettiContext);
9676
+ if (!context) {
9677
+ throw new Error('useConfetti must be used within a Confetti.Provider');
9678
+ }
9679
+ return context;
9680
+ };
9681
+
9682
+ var Confetti$1 = Object.assign(Confetti$2, {
9683
+ Provider: ConfettiProvider,
9684
+ useConfetti: useConfetti
9685
+ });
9686
+
9419
9687
  var Container$1 = index$c(reactNative.View)(function (_ref) {
9420
9688
  var theme = _ref.theme,
9421
9689
  _ref$themeVariant = _ref.themeVariant,
@@ -23137,6 +23405,7 @@ var Confetti = function Confetti(_ref) {
23137
23405
  strokeWidth: "0.4"
23138
23406
  })));
23139
23407
  };
23408
+ Confetti.displayName = 'ConfettiIllustration';
23140
23409
 
23141
23410
  var Connections = function Connections(_ref) {
23142
23411
  var stroke = _ref.stroke,
@@ -43623,6 +43892,7 @@ exports.Chart = Chart;
43623
43892
  exports.Checkbox = index$9;
43624
43893
  exports.Chip = Chip;
43625
43894
  exports.Collapse = Collapse;
43895
+ exports.Confetti = Confetti$1;
43626
43896
  exports.ContentNavigator = ContentNavigator;
43627
43897
  exports.DatePicker = index$8;
43628
43898
  exports.Divider = Divider;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hero-design/rn",
3
- "version": "8.140.0",
3
+ "version": "8.141.0",
4
4
  "license": "MIT",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",
@@ -0,0 +1,239 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import {
9
+ AccessibilityInfo,
10
+ Animated,
11
+ StyleSheet,
12
+ useWindowDimensions,
13
+ } from 'react-native';
14
+ import type { LayoutChangeEvent } from 'react-native';
15
+
16
+ import { useTheme } from '../../theme';
17
+ import { computeXKeyframes } from './computeXKeyframes';
18
+
19
+ const CONFETTI_MIN_SIZE = 4;
20
+ const CONFETTI_MAX_SIZE = 20;
21
+ const BATCH_SIZE = 10;
22
+ const BATCH_DELAY_MS = 300;
23
+ const FADE_OUT_MS = 500;
24
+ const Y_START_OFFSET = -60;
25
+
26
+ const styles = StyleSheet.create({
27
+ container: {
28
+ ...StyleSheet.absoluteFillObject,
29
+ overflow: 'hidden',
30
+ },
31
+ piece: {
32
+ position: 'absolute',
33
+ left: 0,
34
+ top: 0,
35
+ },
36
+ });
37
+
38
+ function randomBetween(min: number, max: number): number {
39
+ return Math.random() * (max - min) + min;
40
+ }
41
+
42
+ type PieceConfig = {
43
+ color: string;
44
+ size: number;
45
+ initialX: number;
46
+ xVel: number;
47
+ yVel: number;
48
+ angleVel: number;
49
+ delay: number;
50
+ };
51
+
52
+ type ConfettiPieceProps = PieceConfig & {
53
+ containerWidth: number;
54
+ duration: number;
55
+ onFinish: () => void;
56
+ };
57
+
58
+ const ConfettiPiece = ({
59
+ angleVel,
60
+ color,
61
+ containerWidth,
62
+ delay,
63
+ duration,
64
+ initialX,
65
+ onFinish,
66
+ size,
67
+ xVel,
68
+ yVel,
69
+ }: ConfettiPieceProps) => {
70
+ const progress = useRef(new Animated.Value(0)).current;
71
+ const durationSeconds = duration / 1000;
72
+
73
+ const xKeyframes = useMemo(
74
+ () => computeXKeyframes(initialX, xVel, durationSeconds, containerWidth),
75
+ // eslint-disable-next-line react-hooks/exhaustive-deps
76
+ []
77
+ );
78
+
79
+ const translateX = progress.interpolate({
80
+ inputRange: xKeyframes.inputRange,
81
+ outputRange: xKeyframes.outputRange,
82
+ });
83
+
84
+ const translateY = progress.interpolate({
85
+ inputRange: [0, 1],
86
+ outputRange: [Y_START_OFFSET, Y_START_OFFSET + yVel * durationSeconds],
87
+ });
88
+
89
+ const rotate = progress.interpolate({
90
+ inputRange: [0, 1],
91
+ outputRange: ['0rad', `${angleVel * durationSeconds}rad`],
92
+ });
93
+
94
+ useEffect(() => {
95
+ Animated.sequence([
96
+ Animated.delay(delay),
97
+ Animated.timing(progress, {
98
+ toValue: 1,
99
+ duration,
100
+ useNativeDriver: true,
101
+ }),
102
+ ]).start(({ finished }) => {
103
+ if (finished) onFinish();
104
+ });
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, []);
107
+
108
+ return (
109
+ <Animated.View
110
+ style={[
111
+ styles.piece,
112
+ { width: size, height: size / 2, backgroundColor: color },
113
+ { transform: [{ translateX }, { translateY }, { rotate }] },
114
+ ]}
115
+ />
116
+ );
117
+ };
118
+
119
+ export type ShowConfettiConfig = {
120
+ /** Animation duration in milliseconds.
121
+ * @default 3000
122
+ */
123
+ duration?: number;
124
+ /** Number of confetti pieces to render.
125
+ * @default 80
126
+ */
127
+ numberOfPieces?: number;
128
+ /** Colors for confetti pieces. Defaults to theme primary and secondary colors. */
129
+ colors?: string[];
130
+ };
131
+
132
+ export type ConfettiProps = ShowConfettiConfig & {
133
+ /** Triggers the confetti burst animation when set to true */
134
+ run: boolean;
135
+ /** Called after all pieces have finished animating and faded out */
136
+ onFinish?: () => void;
137
+ testID?: string;
138
+ };
139
+
140
+ const Confetti = ({
141
+ colors: colorsProp,
142
+ numberOfPieces = 80,
143
+ duration = 3000,
144
+ onFinish,
145
+ run,
146
+ testID,
147
+ }: ConfettiProps) => {
148
+ const theme = useTheme();
149
+ const { height: screenHeight } = useWindowDimensions();
150
+ const [isReducedMotion, setIsReducedMotion] = useState(false);
151
+ const [isVisible, setIsVisible] = useState(false);
152
+ const [containerWidth, setContainerWidth] = useState(0);
153
+ const containerOpacity = useRef(new Animated.Value(0)).current;
154
+ const finishedCount = useRef(0);
155
+ const pieces = useRef<PieceConfig[] | null>(null);
156
+
157
+ useEffect(() => {
158
+ AccessibilityInfo.isReduceMotionEnabled().then(setIsReducedMotion);
159
+ const sub = AccessibilityInfo.addEventListener(
160
+ 'reduceMotionChanged',
161
+ setIsReducedMotion
162
+ );
163
+ return () => sub.remove();
164
+ }, []);
165
+
166
+ useEffect(() => {
167
+ if (run && !isReducedMotion && containerWidth > 0) {
168
+ const durationSeconds = duration / 1000;
169
+ const yVelMin = (screenHeight + 80) / durationSeconds;
170
+ const yVelMax = yVelMin * 1.5;
171
+ const colors = colorsProp ?? [
172
+ theme.colors.primary,
173
+ theme.colors.secondary,
174
+ ];
175
+ pieces.current = Array.from({ length: numberOfPieces }, (_, i) => ({
176
+ color: colors[i % colors.length],
177
+ size: Math.round(randomBetween(CONFETTI_MIN_SIZE, CONFETTI_MAX_SIZE)),
178
+ initialX: randomBetween(containerWidth * 0.2, containerWidth * 0.8),
179
+ xVel: randomBetween(-200, 200),
180
+ yVel: randomBetween(yVelMin, yVelMax),
181
+ angleVel: randomBetween(-Math.PI * 1.5, Math.PI * 1.5),
182
+ delay: Math.floor(i / BATCH_SIZE) * BATCH_DELAY_MS,
183
+ }));
184
+ finishedCount.current = 0;
185
+ containerOpacity.setValue(1);
186
+ setIsVisible(true);
187
+ }
188
+ // eslint-disable-next-line react-hooks/exhaustive-deps
189
+ }, [run, containerWidth]);
190
+
191
+ const handlePieceFinish = useCallback(() => {
192
+ finishedCount.current += 1;
193
+ if (finishedCount.current >= numberOfPieces) {
194
+ Animated.timing(containerOpacity, {
195
+ toValue: 0,
196
+ duration: FADE_OUT_MS,
197
+ useNativeDriver: true,
198
+ }).start(({ finished }) => {
199
+ if (finished) {
200
+ setIsVisible(false);
201
+ onFinish?.();
202
+ }
203
+ });
204
+ }
205
+ }, [containerOpacity, numberOfPieces, onFinish]);
206
+
207
+ const handleLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
208
+ const { width } = nativeEvent.layout;
209
+ if (width > 0) setContainerWidth(width);
210
+ }, []);
211
+
212
+ if (isReducedMotion) {
213
+ return null;
214
+ }
215
+
216
+ // Always render the container (when not in reduced motion) so onLayout fires and we know the real width.
217
+ // Visibility is controlled via opacity to avoid losing the measurement.
218
+ return (
219
+ <Animated.View
220
+ pointerEvents="none"
221
+ onLayout={handleLayout}
222
+ style={[styles.container, { opacity: isVisible ? containerOpacity : 0 }]}
223
+ testID={isVisible ? testID : undefined}
224
+ >
225
+ {isVisible &&
226
+ pieces.current?.map((piece, id) => (
227
+ <ConfettiPiece
228
+ key={id}
229
+ {...piece}
230
+ containerWidth={containerWidth}
231
+ duration={duration}
232
+ onFinish={handlePieceFinish}
233
+ />
234
+ ))}
235
+ </Animated.View>
236
+ );
237
+ };
238
+
239
+ export default Confetti;
@@ -0,0 +1,61 @@
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import type { PropsWithChildren } from 'react';
9
+
10
+ import Confetti from './Confetti';
11
+ import type { ShowConfettiConfig } from './Confetti';
12
+
13
+ type ConfettiContextValue = {
14
+ showConfetti: (
15
+ config?: ShowConfettiConfig,
16
+ onFinished?: (isFinished: boolean) => void
17
+ ) => void;
18
+ };
19
+
20
+ export const ConfettiContext = createContext<ConfettiContextValue | null>(null);
21
+
22
+ const ConfettiProvider = ({ children }: PropsWithChildren) => {
23
+ const [run, setRun] = useState(false);
24
+ const [callConfig, setCallConfig] = useState<
25
+ ShowConfettiConfig | undefined
26
+ >();
27
+ const onFinishedRef = useRef<((isFinished: boolean) => void) | undefined>(
28
+ undefined
29
+ );
30
+
31
+ const showConfetti = useCallback(
32
+ (
33
+ config?: ShowConfettiConfig,
34
+ onFinished?: (isFinished: boolean) => void
35
+ ) => {
36
+ setCallConfig(config);
37
+ onFinishedRef.current = onFinished;
38
+ setRun(true);
39
+ },
40
+ []
41
+ );
42
+
43
+ const handleFinish = useCallback(() => {
44
+ setRun(false);
45
+ onFinishedRef.current?.(true);
46
+ }, []);
47
+
48
+ const contextValue = useMemo<ConfettiContextValue>(
49
+ () => ({ showConfetti }),
50
+ [showConfetti]
51
+ );
52
+
53
+ return (
54
+ <ConfettiContext.Provider value={contextValue}>
55
+ {children}
56
+ <Confetti run={run} onFinish={handleFinish} {...callConfig} />
57
+ </ConfettiContext.Provider>
58
+ );
59
+ };
60
+
61
+ export default ConfettiProvider;
@@ -0,0 +1,38 @@
1
+ export function reflectX(rawX: number, width: number): number {
2
+ const period = 2 * width;
3
+ const modX = ((rawX % period) + period) % period;
4
+ return modX < width ? modX : period - modX;
5
+ }
6
+
7
+ /**
8
+ * Pre-computes the reflectX triangle-wave as interpolation keyframes.
9
+ * Run once in JS before animation starts; native driver handles per-frame lerp.
10
+ */
11
+ export function computeXKeyframes(
12
+ initialX: number,
13
+ xVel: number,
14
+ durationSeconds: number,
15
+ width: number
16
+ ): { inputRange: number[]; outputRange: number[] } {
17
+ if (xVel === 0) {
18
+ return { inputRange: [0, 1], outputRange: [initialX, initialX] };
19
+ }
20
+ const rawEnd = initialX + xVel * durationSeconds;
21
+ const rawMin = Math.min(initialX, rawEnd);
22
+ const rawMax = Math.max(initialX, rawEnd);
23
+ const nMin = Math.ceil(rawMin / width);
24
+ const nMax = Math.floor(rawMax / width);
25
+
26
+ const times: number[] = [0];
27
+ for (let n = nMin; n <= nMax; n += 1) {
28
+ const t = (n * width - initialX) / xVel;
29
+ if (t > 0 && t < durationSeconds) times.push(t);
30
+ }
31
+ times.push(durationSeconds);
32
+ times.sort((a, b) => a - b);
33
+
34
+ return {
35
+ inputRange: times.map((t) => t / durationSeconds),
36
+ outputRange: times.map((t) => reflectX(initialX + xVel * t, width)),
37
+ };
38
+ }
@@ -0,0 +1,12 @@
1
+ import ConfettiComponent from './Confetti';
2
+ import ConfettiProvider from './ConfettiProvider';
3
+ import useConfetti from './useConfetti';
4
+
5
+ export type { ConfettiProps, ShowConfettiConfig } from './Confetti';
6
+
7
+ const Confetti = Object.assign(ConfettiComponent, {
8
+ Provider: ConfettiProvider,
9
+ useConfetti,
10
+ });
11
+
12
+ export default Confetti;
@@ -0,0 +1,13 @@
1
+ import { useContext } from 'react';
2
+
3
+ import { ConfettiContext } from './ConfettiProvider';
4
+
5
+ const useConfetti = () => {
6
+ const context = useContext(ConfettiContext);
7
+ if (!context) {
8
+ throw new Error('useConfetti must be used within a Confetti.Provider');
9
+ }
10
+ return context;
11
+ };
12
+
13
+ export default useConfetti;
@@ -82,4 +82,6 @@ const Confetti = ({
82
82
  );
83
83
  };
84
84
 
85
+ Confetti.displayName = 'ConfettiIllustration';
86
+
85
87
  export default Confetti;
package/src/index.ts CHANGED
@@ -24,6 +24,8 @@ import type { ShadowPalette } from './theme';
24
24
  import { scale } from './utils/scale';
25
25
 
26
26
  import Accordion from './components/Accordion';
27
+ import Confetti from './components/Confetti';
28
+ import type { ConfettiProps } from './components/Confetti';
27
29
  import Alert from './components/Alert';
28
30
  import AppCue from './components/AppCue';
29
31
  import Attachment from './components/Attachment';
@@ -183,6 +185,8 @@ export {
183
185
  FilterTrigger,
184
186
  InlineLoader,
185
187
  type InlineLoaderProps,
188
+ Confetti,
189
+ type ConfettiProps,
186
190
  styled,
187
191
  };
188
192
 
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ export type ShowConfettiConfig = {
3
+ /** Animation duration in milliseconds.
4
+ * @default 3000
5
+ */
6
+ duration?: number;
7
+ /** Number of confetti pieces to render.
8
+ * @default 80
9
+ */
10
+ numberOfPieces?: number;
11
+ /** Colors for confetti pieces. Defaults to theme primary and secondary colors. */
12
+ colors?: string[];
13
+ };
14
+ export type ConfettiProps = ShowConfettiConfig & {
15
+ /** Triggers the confetti burst animation when set to true */
16
+ run: boolean;
17
+ /** Called after all pieces have finished animating and faded out */
18
+ onFinish?: () => void;
19
+ testID?: string;
20
+ };
21
+ declare const Confetti: ({ colors: colorsProp, numberOfPieces, duration, onFinish, run, testID, }: ConfettiProps) => React.JSX.Element | null;
22
+ export default Confetti;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { PropsWithChildren } from 'react';
3
+ import type { ShowConfettiConfig } from './Confetti';
4
+ type ConfettiContextValue = {
5
+ showConfetti: (config?: ShowConfettiConfig, onFinished?: (isFinished: boolean) => void) => void;
6
+ };
7
+ export declare const ConfettiContext: React.Context<ConfettiContextValue | null>;
8
+ declare const ConfettiProvider: ({ children }: PropsWithChildren) => React.JSX.Element;
9
+ export default ConfettiProvider;
@@ -0,0 +1,9 @@
1
+ export declare function reflectX(rawX: number, width: number): number;
2
+ /**
3
+ * Pre-computes the reflectX triangle-wave as interpolation keyframes.
4
+ * Run once in JS before animation starts; native driver handles per-frame lerp.
5
+ */
6
+ export declare function computeXKeyframes(initialX: number, xVel: number, durationSeconds: number, width: number): {
7
+ inputRange: number[];
8
+ outputRange: number[];
9
+ };
@@ -0,0 +1,8 @@
1
+ export type { ConfettiProps, ShowConfettiConfig } from './Confetti';
2
+ declare const Confetti: (({ colors: colorsProp, numberOfPieces, duration, onFinish, run, testID, }: import("./Confetti").ConfettiProps) => import("react").JSX.Element | null) & {
3
+ Provider: ({ children }: import("react").PropsWithChildren) => import("react").JSX.Element;
4
+ useConfetti: () => {
5
+ showConfetti: (config?: import("./Confetti").ShowConfettiConfig, onFinished?: (isFinished: boolean) => void) => void;
6
+ };
7
+ };
8
+ export default Confetti;
@@ -0,0 +1,4 @@
1
+ declare const useConfetti: () => {
2
+ showConfetti: (config?: import("./Confetti").ShowConfettiConfig, onFinished?: (isFinished: boolean) => void) => void;
3
+ };
4
+ export default useConfetti;
@@ -1,4 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { IllustrationSvgProps } from '../types';
3
- declare const Confetti: ({ stroke, fill, testID, width, height, }: IllustrationSvgProps) => React.JSX.Element;
3
+ declare const Confetti: {
4
+ ({ stroke, fill, testID, width, height, }: IllustrationSvgProps): React.JSX.Element;
5
+ displayName: string;
6
+ };
4
7
  export default Confetti;
@@ -11,7 +11,10 @@ export declare const Illustrations: {
11
11
  readonly search: ({ stroke, fill, testID, width, height, }: import("./types").IllustrationSvgProps) => import("react").JSX.Element;
12
12
  readonly star: ({ stroke, fill, testID, width, height, }: import("./types").IllustrationSvgProps) => import("react").JSX.Element;
13
13
  readonly user: ({ stroke, fill, testID, width, height, }: import("./types").IllustrationSvgProps) => import("react").JSX.Element;
14
- readonly confetti: ({ stroke, fill, testID, width, height, }: import("./types").IllustrationSvgProps) => import("react").JSX.Element;
14
+ readonly confetti: {
15
+ ({ stroke, fill, testID, width, height, }: import("./types").IllustrationSvgProps): import("react").JSX.Element;
16
+ displayName: string;
17
+ };
15
18
  readonly error: ({ stroke, fill, testID, width, height, }: import("./types").IllustrationSvgProps) => import("react").JSX.Element;
16
19
  readonly info: ({ stroke, fill, testID, width, height, }: import("./types").IllustrationSvgProps) => import("react").JSX.Element;
17
20
  readonly success: ({ stroke, fill, testID, width, height, }: import("./types").IllustrationSvgProps) => import("react").JSX.Element;
package/types/index.d.ts CHANGED
@@ -3,6 +3,8 @@ import theme, { getTheme, ThemeProvider, useTheme, swagSystemPalette, swagLightS
3
3
  import type { ShadowPalette } from './theme';
4
4
  import { scale } from './utils/scale';
5
5
  import Accordion from './components/Accordion';
6
+ import Confetti from './components/Confetti';
7
+ import type { ConfettiProps } from './components/Confetti';
6
8
  import Alert from './components/Alert';
7
9
  import AppCue from './components/AppCue';
8
10
  import Attachment from './components/Attachment';
@@ -63,6 +65,6 @@ import FloatingIsland from './components/FloatingIsland';
63
65
  import LocaleProvider from './components/LocaleProvider';
64
66
  import FilterTrigger from './components/FilterTrigger';
65
67
  import InlineLoader, { type InlineLoaderProps } from './components/InlineLoader';
66
- export { theme, getTheme, useTheme, scale, ThemeProvider, ThemeSwitcher, withTheme, swagSystemPalette, swagLightSystemPalette, swagLightJobsSystemPalette, swagDarkSystemPalette, workSystemPalette, jobsSystemPalette, walletSystemPalette, eBensSystemPalette, ehWorkDarkSystemPalette, ehWorkSystemPalette, ehJobsSystemPalette, ehWorkShadowPalette, ehJobsShadowPalette, ehWorkDarkShadowPalette, Accordion, Alert, AppCue, Attachment, Avatar, useAvatarColors, Badge, BottomNavigation, BottomSheet, Box, Button, Calendar, Card, Chart, Carousel, Chip, Collapse, Checkbox, ContentNavigator, DatePicker, Divider, Drawer, Empty, Error, FAB, FlatListWithFAB, Icon, Illustration, type IllustrationName, IllustrationList, Image, HeroDesignProvider, MapPin, List, PinInput, Progress, Portal, PageControl, Skeleton, Slider, Spinner, Swipeable, Radio, Search, SegmentedControl, ScrollViewWithFAB, SectionHeading, SectionList, SectionListWithFAB, Select, Success, Switch, Tabs, Tag, TextInput, TimePicker, Toast, Toolbar, Typography, Rate, RefreshControl, RichTextEditor, FloatingIsland, LocaleProvider, FilterTrigger, InlineLoader, type InlineLoaderProps, styled, };
68
+ export { theme, getTheme, useTheme, scale, ThemeProvider, ThemeSwitcher, withTheme, swagSystemPalette, swagLightSystemPalette, swagLightJobsSystemPalette, swagDarkSystemPalette, workSystemPalette, jobsSystemPalette, walletSystemPalette, eBensSystemPalette, ehWorkDarkSystemPalette, ehWorkSystemPalette, ehJobsSystemPalette, ehWorkShadowPalette, ehJobsShadowPalette, ehWorkDarkShadowPalette, Accordion, Alert, AppCue, Attachment, Avatar, useAvatarColors, Badge, BottomNavigation, BottomSheet, Box, Button, Calendar, Card, Chart, Carousel, Chip, Collapse, Checkbox, ContentNavigator, DatePicker, Divider, Drawer, Empty, Error, FAB, FlatListWithFAB, Icon, Illustration, type IllustrationName, IllustrationList, Image, HeroDesignProvider, MapPin, List, PinInput, Progress, Portal, PageControl, Skeleton, Slider, Spinner, Swipeable, Radio, Search, SegmentedControl, ScrollViewWithFAB, SectionHeading, SectionList, SectionListWithFAB, Select, Success, Switch, Tabs, Tag, TextInput, TimePicker, Toast, Toolbar, Typography, Rate, RefreshControl, RichTextEditor, FloatingIsland, LocaleProvider, FilterTrigger, InlineLoader, type InlineLoaderProps, Confetti, type ConfettiProps, styled, };
67
69
  export * from './types';
68
70
  export type { ShadowPalette };