├── .gitignore ├── src ├── hooks │ ├── index.ts │ └── usePrevious.ts ├── util │ ├── index.ts │ └── formatForDisplay.ts ├── index.ts ├── declarations.d.ts ├── styles.css └── AnimatedCounter.tsx ├── dist ├── cjs │ ├── hooks │ │ ├── index.d.ts │ │ ├── usePrevious.d.ts │ │ ├── index.js.map │ │ ├── usePrevious.js.map │ │ ├── usePrevious.js │ │ └── index.js │ ├── util │ │ ├── index.d.ts │ │ ├── calculateDigitWidth.d.ts │ │ ├── index.js.map │ │ ├── formatForDisplay.d.ts │ │ ├── calculateDigitWidth.js.map │ │ ├── calculateDigitWidth.js │ │ ├── formatForDisplay.js │ │ ├── formatForDisplay.js.map │ │ └── index.js │ ├── index.d.ts │ ├── index.js.map │ ├── index.js │ ├── styles.css │ ├── AnimatedCounter.d.ts │ ├── AnimatedCounter.js.map │ └── AnimatedCounter.js └── esm │ ├── hooks │ ├── index.d.ts │ ├── index.js │ ├── usePrevious.d.ts │ ├── index.js.map │ ├── usePrevious.js.map │ └── usePrevious.js │ ├── util │ ├── index.d.ts │ ├── index.js │ ├── calculateDigitWidth.d.ts │ ├── index.js.map │ ├── formatForDisplay.d.ts │ ├── calculateDigitWidth.js │ ├── calculateDigitWidth.js.map │ ├── formatForDisplay.js │ └── formatForDisplay.js.map │ ├── index.d.ts │ ├── index.js │ ├── index.js.map │ ├── styles.css │ ├── AnimatedCounter.d.ts │ ├── AnimatedCounter.js.map │ └── AnimatedCounter.js ├── .DS_Store ├── babel.config.js ├── webpack.config.js ├── tsconfig.json ├── rollup.config.mjs ├── LICENSE ├── package.json ├── README.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | yarn-error.log -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as usePrevious } from './usePrevious'; -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export { default as formatForDisplay } from './formatForDisplay'; -------------------------------------------------------------------------------- /dist/cjs/hooks/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as usePrevious } from './usePrevious'; 2 | -------------------------------------------------------------------------------- /dist/esm/hooks/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as usePrevious } from './usePrevious'; 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuckerMassad/react-animated-counter/HEAD/.DS_Store -------------------------------------------------------------------------------- /dist/cjs/util/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as formatForDisplay } from './formatForDisplay'; 2 | -------------------------------------------------------------------------------- /dist/esm/util/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as formatForDisplay } from './formatForDisplay'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import AnimatedCounter from './AnimatedCounter'; 2 | 3 | export { AnimatedCounter }; -------------------------------------------------------------------------------- /dist/cjs/index.d.ts: -------------------------------------------------------------------------------- 1 | import AnimatedCounter from './AnimatedCounter'; 2 | export { AnimatedCounter }; 3 | -------------------------------------------------------------------------------- /dist/esm/index.d.ts: -------------------------------------------------------------------------------- 1 | import AnimatedCounter from './AnimatedCounter'; 2 | export { AnimatedCounter }; 3 | -------------------------------------------------------------------------------- /dist/esm/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as usePrevious } from './usePrevious'; 2 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/esm/util/index.js: -------------------------------------------------------------------------------- 1 | export { default as formatForDisplay } from './formatForDisplay'; 2 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } -------------------------------------------------------------------------------- /dist/cjs/hooks/usePrevious.d.ts: -------------------------------------------------------------------------------- 1 | declare const usePrevious: (value: number | null) => number | null; 2 | export default usePrevious; 3 | -------------------------------------------------------------------------------- /dist/esm/hooks/usePrevious.d.ts: -------------------------------------------------------------------------------- 1 | declare const usePrevious: (value: number | null) => number | null; 2 | export default usePrevious; 3 | -------------------------------------------------------------------------------- /dist/esm/index.js: -------------------------------------------------------------------------------- 1 | import AnimatedCounter from './AnimatedCounter'; 2 | export { AnimatedCounter }; 3 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/cjs/util/calculateDigitWidth.d.ts: -------------------------------------------------------------------------------- 1 | declare const calculateDigitWidth: (digit: number) => "50%" | "80%" | "100%"; 2 | export default calculateDigitWidth; 3 | -------------------------------------------------------------------------------- /dist/esm/util/calculateDigitWidth.d.ts: -------------------------------------------------------------------------------- 1 | declare const calculateDigitWidth: (digit: number) => "50%" | "80%" | "100%"; 2 | export default calculateDigitWidth; 3 | -------------------------------------------------------------------------------- /dist/cjs/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;AAAA,8EAAgD;AAEvC,0BAFF,4BAAe,CAEE"} -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | plugins: ['@babel/plugin-syntax-jsx'] 7 | } -------------------------------------------------------------------------------- /dist/cjs/util/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/util/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,uDAAiE;AAAxD,4EAA2B"} -------------------------------------------------------------------------------- /dist/cjs/hooks/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,6CAAuD;AAA9C,kEAAsB"} -------------------------------------------------------------------------------- /dist/esm/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,CAAC"} -------------------------------------------------------------------------------- /dist/esm/hooks/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC"} -------------------------------------------------------------------------------- /dist/esm/util/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/util/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"} -------------------------------------------------------------------------------- /dist/cjs/util/formatForDisplay.d.ts: -------------------------------------------------------------------------------- 1 | declare const formatForDisplay: (number: number, includeDecimals: boolean, decimalPrecision: number, includeCommas: boolean) => string[]; 2 | export default formatForDisplay; 3 | -------------------------------------------------------------------------------- /dist/esm/util/formatForDisplay.d.ts: -------------------------------------------------------------------------------- 1 | declare const formatForDisplay: (number: number, includeDecimals: boolean, decimalPrecision: number, includeCommas: boolean) => string[]; 2 | export default formatForDisplay; 3 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.js$/, 6 | enforce: "pre", 7 | use: ["source-map-loader"], 8 | }, 9 | ], 10 | }, 11 | ignoreWarnings: [/Failed to parse source map/], 12 | }; -------------------------------------------------------------------------------- /dist/cjs/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.AnimatedCounter = void 0; 4 | var tslib_1 = require("tslib"); 5 | var AnimatedCounter_1 = tslib_1.__importDefault(require("./AnimatedCounter")); 6 | exports.AnimatedCounter = AnimatedCounter_1["default"]; 7 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/esm/util/calculateDigitWidth.js: -------------------------------------------------------------------------------- 1 | // Adjusts width of individual narrow digits 2 | var calculateDigitWidth = function (digit) { 3 | switch ("".concat(digit)) { 4 | case '1': 5 | return '50%'; 6 | case '7': 7 | return '80%'; 8 | default: 9 | return '100%'; 10 | } 11 | }; 12 | export default calculateDigitWidth; -------------------------------------------------------------------------------- /dist/cjs/util/calculateDigitWidth.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"calculateDigitWidth.js","sourceRoot":"","sources":["../../../src/util/calculateDigitWidth.ts"],"names":[],"mappings":";;AAAA,6CAA6C;AAC7C,IAAM,mBAAmB,GAAG,UAAC,KAAa;IACxC,QAAQ,UAAG,KAAK,CAAE,EAAE;QAClB,KAAK,GAAG;YACN,OAAO,KAAK,CAAA;QACd,KAAK,GAAG;YACN,OAAO,KAAK,CAAA;QACd;YACE,OAAO,MAAM,CAAA;KAChB;AACH,CAAC,CAAA;AAED,qBAAe,mBAAmB,CAAC"} -------------------------------------------------------------------------------- /dist/esm/util/calculateDigitWidth.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"calculateDigitWidth.js","sourceRoot":"","sources":["../../../src/util/calculateDigitWidth.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,IAAM,mBAAmB,GAAG,UAAC,KAAa;IACxC,QAAQ,UAAG,KAAK,CAAE,EAAE;QAClB,KAAK,GAAG;YACN,OAAO,KAAK,CAAA;QACd,KAAK,GAAG;YACN,OAAO,KAAK,CAAA;QACd;YACE,OAAO,MAAM,CAAA;KAChB;AACH,CAAC,CAAA;AAED,eAAe,mBAAmB,CAAC"} -------------------------------------------------------------------------------- /dist/cjs/hooks/usePrevious.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"usePrevious.js","sourceRoot":"","sources":["../../../src/hooks/usePrevious.ts"],"names":[],"mappings":";;AAAA,+BAA0C;AAE1C,mHAAmH;AACnH,IAAM,WAAW,GAAG,UAAC,KAAoB;IACvC,IAAM,GAAG,GAAG,IAAA,cAAM,EAAgB,IAAI,CAAC,CAAC;IACxC,IAAA,iBAAS,EAAC;QACR,GAAG,CAAC,OAAO,GAAG,KAAK,CAAC;IACtB,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IACX,OAAO,GAAG,CAAC,OAAO,CAAC;AACrB,CAAC,CAAA;AAED,qBAAe,WAAW,CAAC"} -------------------------------------------------------------------------------- /src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | // Hook used to track previous value of primary number state in AnimatedCounter & individual digits in NumberColumn 4 | const usePrevious = (value: number | null) => { 5 | const ref = useRef(null); 6 | useEffect(() => { 7 | ref.current = value; 8 | }, [value]) 9 | return ref.current; 10 | } 11 | 12 | export default usePrevious; -------------------------------------------------------------------------------- /dist/cjs/util/calculateDigitWidth.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | // Adjusts width of individual narrow digits 4 | var calculateDigitWidth = function (digit) { 5 | switch ("".concat(digit)) { 6 | case '1': 7 | return '50%'; 8 | case '7': 9 | return '80%'; 10 | default: 11 | return '100%'; 12 | } 13 | }; 14 | exports["default"] = calculateDigitWidth; -------------------------------------------------------------------------------- /dist/esm/hooks/usePrevious.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"usePrevious.js","sourceRoot":"","sources":["../../../src/hooks/usePrevious.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE1C,mHAAmH;AACnH,IAAM,WAAW,GAAG,UAAC,KAAoB;IACvC,IAAM,GAAG,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IACxC,SAAS,CAAC;QACR,GAAG,CAAC,OAAO,GAAG,KAAK,CAAC;IACtB,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IACX,OAAO,GAAG,CAAC,OAAO,CAAC;AACrB,CAAC,CAAA;AAED,eAAe,WAAW,CAAC"} -------------------------------------------------------------------------------- /dist/esm/hooks/usePrevious.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | // Hook used to track previous value of primary number state in AnimatedCounter & individual digits in NumberColumn 3 | var usePrevious = function (value) { 4 | var ref = useRef(null); 5 | useEffect(function () { 6 | ref.current = value; 7 | }, [value]); 8 | return ref.current; 9 | }; 10 | export default usePrevious; 11 | //# sourceMappingURL=usePrevious.js.map -------------------------------------------------------------------------------- /dist/cjs/hooks/usePrevious.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var react_1 = require("react"); 4 | // Hook used to track previous value of primary number state in AnimatedCounter & individual digits in NumberColumn 5 | var usePrevious = function (value) { 6 | var ref = (0, react_1.useRef)(null); 7 | (0, react_1.useEffect)(function () { 8 | ref.current = value; 9 | }, [value]); 10 | return ref.current; 11 | }; 12 | exports["default"] = usePrevious; 13 | //# sourceMappingURL=usePrevious.js.map -------------------------------------------------------------------------------- /dist/esm/util/formatForDisplay.js: -------------------------------------------------------------------------------- 1 | // Creates array of digits to vertically scroll through 2 | var formatForDisplay = function (number, includeDecimals, decimalPrecision, includeCommas) { 3 | var decimalCount = includeDecimals ? decimalPrecision : 0; 4 | var parsedNumber = parseFloat("".concat(Math.max(number, 0))).toFixed(decimalCount); 5 | var numberToFormat = includeCommas ? parseFloat(parsedNumber).toLocaleString('en-US', { minimumFractionDigits: includeDecimals ? decimalPrecision : 0 }) : parsedNumber; 6 | return numberToFormat.split('').reverse(); 7 | }; 8 | export default formatForDisplay; 9 | //# sourceMappingURL=formatForDisplay.js.map -------------------------------------------------------------------------------- /src/util/formatForDisplay.ts: -------------------------------------------------------------------------------- 1 | // Creates array of digits to vertically scroll through 2 | const formatForDisplay = ( 3 | number: number, 4 | includeDecimals: boolean, 5 | decimalPrecision: number, 6 | includeCommas: boolean, 7 | ): string[] => { 8 | const decimalCount = includeDecimals ? decimalPrecision : 0; 9 | const parsedNumber = parseFloat(`${Math.max(number, 0)}`).toFixed(decimalCount); 10 | const numberToFormat = includeCommas ? parseFloat(parsedNumber).toLocaleString('en-US', { minimumFractionDigits: includeDecimals ? decimalPrecision : 0 }) : parsedNumber; 11 | return numberToFormat.split('').reverse(); 12 | } 13 | 14 | export default formatForDisplay; -------------------------------------------------------------------------------- /dist/esm/util/formatForDisplay.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"formatForDisplay.js","sourceRoot":"","sources":["../../../src/util/formatForDisplay.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,IAAM,gBAAgB,GAAG,UACvB,MAAc,EACd,eAAwB,EACxB,gBAAwB,EACxB,aAAsB;IAEtB,IAAM,YAAY,GAAG,eAAe,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,IAAM,YAAY,GAAG,UAAU,CAAC,UAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,IAAM,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,qBAAqB,EAAE,eAAe,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;IAC1K,OAAO,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;AAC5C,CAAC,CAAA;AAED,eAAe,gBAAgB,CAAC"} -------------------------------------------------------------------------------- /dist/cjs/util/formatForDisplay.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | // Creates array of digits to vertically scroll through 4 | var formatForDisplay = function (number, includeDecimals, decimalPrecision, includeCommas) { 5 | var decimalCount = includeDecimals ? decimalPrecision : 0; 6 | var parsedNumber = parseFloat("".concat(Math.max(number, 0))).toFixed(decimalCount); 7 | var numberToFormat = includeCommas ? parseFloat(parsedNumber).toLocaleString('en-US', { minimumFractionDigits: includeDecimals ? decimalPrecision : 0 }) : parsedNumber; 8 | return numberToFormat.split('').reverse(); 9 | }; 10 | exports["default"] = formatForDisplay; 11 | //# sourceMappingURL=formatForDisplay.js.map -------------------------------------------------------------------------------- /dist/cjs/util/formatForDisplay.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"formatForDisplay.js","sourceRoot":"","sources":["../../../src/util/formatForDisplay.ts"],"names":[],"mappings":";;AAAA,uDAAuD;AACvD,IAAM,gBAAgB,GAAG,UACvB,MAAc,EACd,eAAwB,EACxB,gBAAwB,EACxB,aAAsB;IAEtB,IAAM,YAAY,GAAG,eAAe,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,IAAM,YAAY,GAAG,UAAU,CAAC,UAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,IAAM,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,qBAAqB,EAAE,eAAe,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;IAC1K,OAAO,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;AAC5C,CAAC,CAAA;AAED,qBAAe,gBAAgB,CAAC"} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "src/*.css" 5 | ], 6 | "exclude": [ 7 | "dist", 8 | "node_modules" 9 | ], 10 | "compilerOptions": { 11 | "module": "esnext", 12 | "lib": ["dom", "esnext"], 13 | "importHelpers": true, 14 | "declaration": true, 15 | "sourceMap": true, 16 | "rootDir": "./src", 17 | "outDir": "./dist/esm", 18 | "strict": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "moduleResolution": "node", 24 | "jsx": "react", 25 | "esModuleInterop": true, 26 | "skipLibCheck": true, 27 | "forceConsistentCasingInFileNames": true, 28 | } 29 | } -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | input: 'src/index.ts', 4 | external: ['react', 'react-dom'], 5 | output: [ 6 | { 7 | file: 'dist/cjs/index.js', 8 | format: 'cjs', 9 | sourcemap: false, 10 | }, 11 | { 12 | file: 'dist/esm/index.js', 13 | format: 'esm', 14 | sourcemap: true, 15 | }, 16 | ], 17 | plugins: [ 18 | resolve(), 19 | commonjs(), 20 | typescript({ tsconfig: './tsconfig.json', sourceMap: false }), 21 | postcss(), 22 | ], 23 | }, 24 | { 25 | input: 'dist/esm/types/index.d.ts', 26 | output: [{ file: 'dist/index.d.ts', format: 'esm' }], 27 | plugins: [dts()], 28 | external: [/\.(css|less|scss)$/], 29 | }, 30 | ]; -------------------------------------------------------------------------------- /dist/cjs/hooks/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | exports.__esModule = true; 14 | exports.usePrevious = void 0; 15 | var usePrevious_1 = require("./usePrevious"); 16 | __createBinding(exports, usePrevious_1, "default", "usePrevious"); 17 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/cjs/util/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | exports.__esModule = true; 14 | exports.formatForDisplay = void 0; 15 | var formatForDisplay_1 = require("./formatForDisplay"); 16 | __createBinding(exports, formatForDisplay_1, "default", "formatForDisplay"); 17 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/esm/styles.css: -------------------------------------------------------------------------------- 1 | .ticker-view { 2 | height: auto; 3 | margin: auto; 4 | display: flex; 5 | flex-direction: row-reverse; 6 | overflow: hidden; 7 | position: relative; 8 | } 9 | 10 | .number-placeholder { 11 | visibility: hidden; 12 | } 13 | 14 | .ticker-column-container { 15 | position: relative; 16 | } 17 | 18 | .ticker-column { 19 | position: absolute; 20 | height: 1000%; 21 | bottom: 0; 22 | } 23 | 24 | .ticker-digit { 25 | width: auto; 26 | height: 10%; 27 | } 28 | 29 | .ticker-column.increase { 30 | animation: pulseIncrement 500ms cubic-bezier(0.4, 0, 0.6, 1) 1; 31 | } 32 | 33 | .ticker-column.decrease { 34 | animation: pulseDecrement 500ms cubic-bezier(0.4, 0, 0.6, 1) 1; 35 | } 36 | 37 | @keyframes pulseIncrement { 38 | 0%, 100% { color: inherit; } 39 | 50% { color: var(--increment-color) } 40 | } 41 | 42 | @keyframes pulseDecrement { 43 | 0%, 100% { color: inherit; } 44 | 50% { color: var(--decrement-color) } 45 | } 46 | -------------------------------------------------------------------------------- /dist/cjs/styles.css: -------------------------------------------------------------------------------- 1 | .ticker-view { 2 | height: auto; 3 | margin: auto; 4 | display: flex; 5 | flex-direction: row-reverse; 6 | overflow: hidden; 7 | position: relative; 8 | } 9 | 10 | .number-placeholder { 11 | visibility: hidden; 12 | } 13 | 14 | .ticker-column-container { 15 | position: relative; 16 | } 17 | 18 | .ticker-column { 19 | position: absolute; 20 | height: 1000%; 21 | bottom: 0; 22 | } 23 | 24 | .ticker-digit { 25 | width: auto; 26 | height: 10%; 27 | } 28 | 29 | .ticker-column.increase { 30 | animation: pulseIncrement 500ms cubic-bezier(0.4, 0, 0.6, 1) 1; 31 | } 32 | 33 | .ticker-column.decrease { 34 | animation: pulseDecrement 500ms cubic-bezier(0.4, 0, 0.6, 1) 1; 35 | } 36 | 37 | @keyframes pulseIncrement { 38 | 0%, 100% { color: inherit; } 39 | 50% { color: var(--increment-color); } 40 | } 41 | 42 | @keyframes pulseDecrement { 43 | 0%, 100% { color: inherit; } 44 | 50% { color: var(--decrement-color); } 45 | } 46 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .ticker-view { 2 | height: auto; 3 | margin: auto; 4 | display: flex; 5 | flex-direction: row-reverse; 6 | overflow: hidden; 7 | position: relative; 8 | } 9 | 10 | .number-placeholder { 11 | visibility: hidden; 12 | } 13 | 14 | .ticker-column-container { 15 | position: relative; 16 | } 17 | 18 | .ticker-column { 19 | position: absolute; 20 | height: 1000%; 21 | bottom: 0; 22 | font-feature-settings: "tnum"; 23 | } 24 | 25 | .ticker-digit { 26 | width: auto; 27 | height: 10%; 28 | } 29 | 30 | .ticker-column.increase { 31 | animation: pulseIncrement 500ms cubic-bezier(0.4, 0, 0.6, 1) 1; 32 | } 33 | 34 | .ticker-column.decrease { 35 | animation: pulseDecrement 500ms cubic-bezier(0.4, 0, 0.6, 1) 1; 36 | } 37 | 38 | @keyframes pulseIncrement { 39 | 0%, 100% { color: inherit; } 40 | 50% { color: var(--increment-color); } 41 | } 42 | 43 | @keyframes pulseDecrement { 44 | 0%, 100% { color: inherit; } 45 | 50% { color: var(--decrement-color); } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tucker Massad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/cjs/AnimatedCounter.d.ts: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | import React, { CSSProperties } from 'react'; 3 | export interface AnimatedCounterProps { 4 | value?: number; 5 | fontSize?: string; 6 | color?: string; 7 | incrementColor?: string; 8 | decrementColor?: string; 9 | includeDecimals?: boolean; 10 | decimalPrecision?: number; 11 | includeCommas?: boolean; 12 | containerStyles?: CSSProperties; 13 | digitStyles?: CSSProperties; 14 | } 15 | export interface NumberColumnProps { 16 | digit: string; 17 | delta: string | null; 18 | fontSize: string; 19 | color: string; 20 | incrementColor: string; 21 | decrementColor: string; 22 | digitStyles: CSSProperties; 23 | } 24 | export interface DecimalColumnProps { 25 | fontSize: string; 26 | color: string; 27 | isComma: boolean; 28 | digitStyles: CSSProperties; 29 | } 30 | declare const _default: React.MemoExoticComponent<({ value, fontSize, color, incrementColor, decrementColor, includeDecimals, decimalPrecision, includeCommas, containerStyles, digitStyles, }: AnimatedCounterProps) => React.JSX.Element>; 31 | export default _default; 32 | -------------------------------------------------------------------------------- /dist/esm/AnimatedCounter.d.ts: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | import React, { CSSProperties } from 'react'; 3 | export interface AnimatedCounterProps { 4 | value?: number; 5 | fontSize?: string; 6 | color?: string; 7 | incrementColor?: string; 8 | decrementColor?: string; 9 | includeDecimals?: boolean; 10 | decimalPrecision?: number; 11 | includeCommas?: boolean; 12 | containerStyles?: CSSProperties; 13 | digitStyles?: CSSProperties; 14 | } 15 | export interface NumberColumnProps { 16 | digit: string; 17 | delta: string | null; 18 | fontSize: string; 19 | color: string; 20 | incrementColor: string; 21 | decrementColor: string; 22 | digitStyles: CSSProperties; 23 | } 24 | export interface DecimalColumnProps { 25 | fontSize: string; 26 | color: string; 27 | isComma: boolean; 28 | digitStyles: CSSProperties; 29 | } 30 | declare const _default: React.MemoExoticComponent<({ value, fontSize, color, incrementColor, decrementColor, includeDecimals, decimalPrecision, includeCommas, containerStyles, digitStyles, }: AnimatedCounterProps) => React.JSX.Element>; 31 | export default _default; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-animated-counter", 3 | "version": "1.8.3", 4 | "description": "React Animated Counter Component", 5 | "main": "./dist/cjs/index.js", 6 | "module": "./dist/esm/index.js", 7 | "types": "./dist/esm/index.d.ts", 8 | "scripts": { 9 | "build": "yarn build:esm && yarn build:cjs", 10 | "build:esm": "tsc", 11 | "build:cjs": "tsc --module commonjs --outDir dist/cjs" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/TuckerMassad/react-animated-counter.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "animated", 20 | "counter", 21 | "components", 22 | "ui", 23 | "number-animation", 24 | "number", 25 | "animation", 26 | "robinhood", 27 | "style", 28 | "up", 29 | "down", 30 | "money", 31 | "counting", 32 | "increment", 33 | "decrement", 34 | "tick", 35 | "ticker" 36 | ], 37 | "author": "tuckermassad", 38 | "license": "ISC", 39 | "bugs": { 40 | "url": "https://github.com/TuckerMassad/react-animated-counter/issues" 41 | }, 42 | "homepage": "https://github.com/TuckerMassad/react-animated-counter#readme", 43 | "peerDependencies": { 44 | "react": ">=16" 45 | }, 46 | "dependencies": { 47 | "framer-motion": "^10.11.4", 48 | "lodash": "^4.17.21" 49 | }, 50 | "devDependencies": { 51 | "@types/lodash": "^4.14.202", 52 | "@types/react": "^18.0.12", 53 | "framer-motion": "^10.11.4", 54 | "react": "^18.2.0", 55 | "react-dom": "^18.2.0", 56 | "rollup-plugin-sourcemaps": "^0.6.3" 57 | }, 58 | "files": [ 59 | "dist", 60 | "LICENSE", 61 | "README.md" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Animated Counter 2 | 3 | A lightweight React component for beautifully animated incrementation & decrementation of a state integer value. Inspired by Robinhood's portfolio balance animation. 4 | 5 | ![react-animated-counter demo](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExb2N4ZG5mcXE1ZWdsZzQ4bnlxdXlvcGcwamQzcWhmNGNvaGNoem14aiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/6qomEsKHcyf6R1YmBs/source.gif) 6 | 7 | Powering today's most innovative web applications like [https://kalshi.com](https://kalshi.com), [https://financhle.com](https://financhle.com), and others. 8 | 9 | ## Installation 10 | 11 | `npm install react-animated-counter` 12 | 13 | ## Usage 14 | 15 | **Props:** 16 | 17 | | Name | Type | Description | Default | 18 | |------------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------|----------------| 19 | | `value` | `integer` | The state integer value | `0` | 20 | | `fontSize` | `string` | Applied css `font-size` | `18px` | 21 | | `color` | `string` | Applied css `color` | `black` | 22 | | `incrementColor` | `string` | Animation color when `value` increases | `#32cd32` | 23 | | `decrementColor` | `string` | Animation color when `value` decreases | `#fe6862` | 24 | |`includeDecimals` | `boolean` | Includes or removes decimal point values in provided `value` (rounds to nearest hundredth by default) | `true` | 25 | |`decimalPrecision`| `boolean` | The nth decimal place of precision (ex. `5` will calculate number to the nearest hundred thousandth) | `2` | 26 | |`includeCommas` | `boolean` | Adds comma separators to every third digit to the left of the decimal point used in numbers with four or more digits | `false` | 27 | |`containerStyles` | `CSSProperties` | Styles to apply to the parent element of the main component. Used in same fashion as react `styles` | `{}` | 28 | |`digitStyles` | `CSSProperties` | Styles to apply to individual digit elements. Used in same fashion as react `styles` | `{}` | 29 | 30 | **Basic Demo:** 31 | 32 | Codesandbox Link: https://codesandbox.io/p/sandbox/clever-water-v5nwwx 33 | 34 | ``` 35 | import React, { useState } from 'react'; 36 | import { AnimatedCounter } from 'react-animated-counter'; 37 | 38 | const App = () => { 39 | // Integer state 40 | const [counterValue, setCounterValue] = useState(500); 41 | 42 | // Handle random increment/decrement 43 | const handleCounterUpdate = (increment) => { 44 | const delta = (Math.floor(Math.random() * 100) + 1) * 0.99; 45 | setCounterValue(increment ? counterValue + delta : counterValue - delta); 46 | }; 47 | 48 | return ( 49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 | ); 57 | }; 58 | ``` 59 | 60 | **Output:** 61 | 62 | ![react-animated-counter demo](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExMzhwbnF0NDU1ZmhsMHRnZnFwdzVycXU5b2MzYnpxZ3ZtZzFhNG0xNyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/N3Xsj09Gp9GbrKF86E/giphy.gif) 63 | 64 | **With `recharts` Demo:** 65 | 66 | Codesandbox Link: https://codesandbox.io/s/suspicious-morning-rx60sm?file=/src/App.js 67 | 68 | **Output:** 69 | 70 | ![react-animated-counter recharts demo](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExMXFoaHkzOG5oMG05aTF6dHo0NHRmOGxmdjQ0Zm1xdGdvNWprNDcyOSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/IJP2ng53lyeF5QXi5T/giphy.gif) 71 | -------------------------------------------------------------------------------- /dist/cjs/AnimatedCounter.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"AnimatedCounter.js","sourceRoot":"","sources":["../../src/AnimatedCounter.tsx"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/esm/AnimatedCounter.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"AnimatedCounter.js","sourceRoot":"","sources":["../../src/AnimatedCounter.tsx"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/esm/AnimatedCounter.js: -------------------------------------------------------------------------------- 1 | import { __assign } from "tslib"; 2 | import './styles.css'; 3 | import React, { memo, useEffect, useRef, useState, useMemo } from 'react'; 4 | import { motion } from "framer-motion"; 5 | import { formatForDisplay } from "./util"; 6 | import { usePrevious } from "./hooks"; 7 | import debounce from 'lodash/debounce'; 8 | // Array of digits to vertically scroll through 9 | var DIGIT_ARRAY = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; 10 | // Decimal element component 11 | var DecimalColumn = memo(function (_a) { 12 | var fontSize = _a.fontSize, color = _a.color, isComma = _a.isComma, digitStyles = _a.digitStyles; 13 | var decimalStyle = useMemo(function () { return (__assign({ fontSize: fontSize, lineHeight: fontSize, color: color, marginLeft: "calc(-".concat(fontSize, " / 10)") }, digitStyles)); }, [fontSize, color, digitStyles]); 14 | return (React.createElement("span", { style: decimalStyle }, isComma ? ',' : '.')); 15 | }); 16 | // Individual number element component 17 | var NumberColumn = memo(function (_a) { 18 | var digit = _a.digit, delta = _a.delta, fontSize = _a.fontSize, color = _a.color, incrementColor = _a.incrementColor, decrementColor = _a.decrementColor, digitStyles = _a.digitStyles; 19 | var fontSizeValue = parseFloat(fontSize.replace('px', '')); 20 | var digitValue = parseInt(digit, 10); 21 | var _b = useState(fontSizeValue * digitValue), position = _b[0], setPosition = _b[1]; 22 | var _c = useState(null), animationClass = _c[0], setAnimationClass = _c[1]; 23 | var currentDigit = +digit; 24 | var previousDigit = usePrevious(+currentDigit); 25 | var columnContainer = useRef(null); 26 | var hasHydrated = useRef(false); 27 | var handleAnimationComplete = useMemo(function () { 28 | return debounce(function () { 29 | setAnimationClass(""); 30 | }, 200); 31 | }, []); 32 | // Update the column position 33 | useEffect(function () { 34 | if (Number.isNaN(digitValue) || Number.isNaN(fontSizeValue)) { 35 | return; 36 | } 37 | // Each 'row' is assumed to be roughly one fontSize tall 38 | var newPosition = fontSizeValue * digitValue; 39 | setPosition(newPosition); 40 | }, [digitValue, fontSizeValue]); 41 | var containerStyle = useMemo(function () { return (__assign({ fontSize: fontSize, lineHeight: fontSize, height: 'auto', color: color, '--increment-color': "".concat(incrementColor), '--decrement-color': "".concat(decrementColor) }, digitStyles)); }, [fontSize, color, incrementColor, decrementColor, digitStyles]); 42 | var digitSpanStyle = useMemo(function () { return (__assign({ fontSize: fontSize, lineHeight: fontSize }, digitStyles)); }, [fontSize, digitStyles]); 43 | var negativeStyle = useMemo(function () { return (__assign({ color: color, fontSize: fontSize, lineHeight: fontSize, marginRight: "calc(".concat(fontSize, " / 5)") }, digitStyles)); }, [color, fontSize, digitStyles]); 44 | useEffect(function () { 45 | if (!hasHydrated.current) { 46 | hasHydrated.current = true; 47 | return; 48 | } 49 | setAnimationClass(previousDigit !== currentDigit ? delta : ''); 50 | }, [digit, delta]); 51 | // If digit is negative symbol, simply return an unanimated character 52 | if (digit === '-') { 53 | return (React.createElement("span", { style: negativeStyle }, digit)); 54 | } 55 | return (React.createElement("div", { className: 'ticker-column-container', ref: columnContainer, style: containerStyle }, 56 | React.createElement(motion.div, __assign({ initial: { x: 0, y: position }, animate: { x: 0, y: position }, className: "ticker-column ".concat(animationClass), onAnimationComplete: handleAnimationComplete }, (!hasHydrated.current && { transition: { duration: 0 } })), DIGIT_ARRAY.map(function (num) { return (React.createElement("div", { className: 'ticker-digit', key: num }, 57 | React.createElement("span", { style: digitSpanStyle }, num))); })), 58 | React.createElement("span", { className: 'number-placeholder' }, "0"))); 59 | }, function (prevProps, nextProps) { 60 | return (prevProps.digit === nextProps.digit && 61 | prevProps.delta === nextProps.delta && 62 | prevProps.fontSize === nextProps.fontSize && 63 | prevProps.color === nextProps.color && 64 | prevProps.incrementColor === nextProps.incrementColor && 65 | prevProps.decrementColor === nextProps.decrementColor && 66 | JSON.stringify(prevProps.digitStyles) === JSON.stringify(nextProps.digitStyles)); 67 | }); 68 | // Main component 69 | var AnimatedCounter = function (_a) { 70 | var _b = _a.value, value = _b === void 0 ? 0 : _b, _c = _a.fontSize, fontSize = _c === void 0 ? '18px' : _c, _d = _a.color, color = _d === void 0 ? 'black' : _d, _e = _a.incrementColor, incrementColor = _e === void 0 ? '#32cd32' : _e, _f = _a.decrementColor, decrementColor = _f === void 0 ? '#fe6862' : _f, _g = _a.includeDecimals, includeDecimals = _g === void 0 ? true : _g, _h = _a.decimalPrecision, decimalPrecision = _h === void 0 ? 2 : _h, _j = _a.includeCommas, includeCommas = _j === void 0 ? false : _j, _k = _a.containerStyles, containerStyles = _k === void 0 ? {} : _k, _l = _a.digitStyles, digitStyles = _l === void 0 ? {} : _l; 71 | var hasInitialRender = useRef(true); 72 | var numArray = useMemo(function () { 73 | return formatForDisplay(Math.abs(value), includeDecimals, decimalPrecision, includeCommas); 74 | }, [value, includeDecimals, decimalPrecision, includeCommas]); 75 | var previousNumber = usePrevious(value); 76 | var isNegative = value < 0; 77 | var delta = useMemo(function () { 78 | if (previousNumber !== null) { 79 | if (value > previousNumber) { 80 | return 'increase'; 81 | } 82 | else if (value < previousNumber) { 83 | return 'decrease'; 84 | } 85 | } 86 | return null; 87 | }, [value, previousNumber]); 88 | // Mark as hydrated after first render 89 | useEffect(function () { 90 | hasInitialRender.current = false; 91 | }, []); 92 | return (React.createElement(motion.div, { className: 'ticker-view', style: containerStyles }, 93 | numArray.map(function (number, index) { 94 | return number === "." || number === "," ? (React.createElement(DecimalColumn, { key: index, fontSize: fontSize, color: color, isComma: number === ",", digitStyles: digitStyles })) : (React.createElement(NumberColumn, { key: index, digit: number, delta: delta, color: color, fontSize: fontSize, incrementColor: incrementColor, decrementColor: decrementColor, digitStyles: digitStyles })); 95 | }), 96 | isNegative && 97 | React.createElement(NumberColumn, { key: 'negative-feedback', digit: '-', delta: delta, color: color, fontSize: fontSize, incrementColor: incrementColor, decrementColor: decrementColor, digitStyles: digitStyles }))); 98 | }; 99 | export default React.memo(AnimatedCounter); 100 | //# sourceMappingURL=AnimatedCounter.js.map -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@emotion/is-prop-valid@^0.8.2": 6 | "integrity" "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==" 7 | "resolved" "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz" 8 | "version" "0.8.8" 9 | dependencies: 10 | "@emotion/memoize" "0.7.4" 11 | 12 | "@emotion/memoize@0.7.4": 13 | "integrity" "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" 14 | "resolved" "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz" 15 | "version" "0.7.4" 16 | 17 | "@rollup/pluginutils@^3.0.9": 18 | "integrity" "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==" 19 | "resolved" "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" 20 | "version" "3.1.0" 21 | dependencies: 22 | "@types/estree" "0.0.39" 23 | "estree-walker" "^1.0.1" 24 | "picomatch" "^2.2.2" 25 | 26 | "@types/estree@0.0.39": 27 | "integrity" "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" 28 | "resolved" "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" 29 | "version" "0.0.39" 30 | 31 | "@types/lodash@^4.14.202": 32 | "integrity" "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" 33 | "resolved" "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz" 34 | "version" "4.14.202" 35 | 36 | "@types/prop-types@*": 37 | "integrity" "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" 38 | "resolved" "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz" 39 | "version" "15.7.9" 40 | 41 | "@types/react@^18.0.12": 42 | "integrity" "sha512-Z+ZrIRocWtdD70j45izShRwDuiB4JZqDegqMFW/I8aG5DxxLKOzVNoq62UIO82v9bdgi+DO1jvsb9sTEZUSm+Q==" 43 | "resolved" "https://registry.npmjs.org/@types/react/-/react-18.2.29.tgz" 44 | "version" "18.2.29" 45 | dependencies: 46 | "@types/prop-types" "*" 47 | "@types/scheduler" "*" 48 | "csstype" "^3.0.2" 49 | 50 | "@types/scheduler@*": 51 | "integrity" "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==" 52 | "resolved" "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz" 53 | "version" "0.16.5" 54 | 55 | "atob@^2.1.2": 56 | "integrity" "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" 57 | "resolved" "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" 58 | "version" "2.1.2" 59 | 60 | "csstype@^3.0.2": 61 | "integrity" "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" 62 | "resolved" "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" 63 | "version" "3.1.2" 64 | 65 | "decode-uri-component@^0.2.0": 66 | "integrity" "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" 67 | "resolved" "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz" 68 | "version" "0.2.2" 69 | 70 | "estree-walker@^1.0.1": 71 | "integrity" "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" 72 | "resolved" "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" 73 | "version" "1.0.1" 74 | 75 | "framer-motion@^10.11.4": 76 | "integrity" "sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA==" 77 | "resolved" "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.4.tgz" 78 | "version" "10.16.4" 79 | dependencies: 80 | "tslib" "^2.4.0" 81 | optionalDependencies: 82 | "@emotion/is-prop-valid" "^0.8.2" 83 | 84 | "fsevents@~2.3.2": 85 | "integrity" "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" 86 | "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" 87 | "version" "2.3.3" 88 | 89 | "js-tokens@^3.0.0 || ^4.0.0": 90 | "integrity" "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 91 | "resolved" "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" 92 | "version" "4.0.0" 93 | 94 | "lodash@^4.17.21": 95 | "integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 96 | "resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" 97 | "version" "4.17.21" 98 | 99 | "loose-envify@^1.1.0": 100 | "integrity" "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==" 101 | "resolved" "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" 102 | "version" "1.4.0" 103 | dependencies: 104 | "js-tokens" "^3.0.0 || ^4.0.0" 105 | 106 | "picomatch@^2.2.2": 107 | "integrity" "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" 108 | "resolved" "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" 109 | "version" "2.3.1" 110 | 111 | "react-dom@^18.0.0", "react-dom@^18.2.0": 112 | "integrity" "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==" 113 | "resolved" "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" 114 | "version" "18.2.0" 115 | dependencies: 116 | "loose-envify" "^1.1.0" 117 | "scheduler" "^0.23.0" 118 | 119 | "react@^18.0.0", "react@^18.2.0": 120 | "integrity" "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==" 121 | "resolved" "https://registry.npmjs.org/react/-/react-18.2.0.tgz" 122 | "version" "18.2.0" 123 | dependencies: 124 | "loose-envify" "^1.1.0" 125 | 126 | "rollup-plugin-sourcemaps@^0.6.3": 127 | "integrity" "sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==" 128 | "resolved" "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz" 129 | "version" "0.6.3" 130 | dependencies: 131 | "@rollup/pluginutils" "^3.0.9" 132 | "source-map-resolve" "^0.6.0" 133 | 134 | "rollup@^1.20.0||^2.0.0", "rollup@>=0.31.2": 135 | "integrity" "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==" 136 | "resolved" "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz" 137 | "version" "2.79.1" 138 | optionalDependencies: 139 | "fsevents" "~2.3.2" 140 | 141 | "scheduler@^0.23.0": 142 | "integrity" "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==" 143 | "resolved" "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz" 144 | "version" "0.23.0" 145 | dependencies: 146 | "loose-envify" "^1.1.0" 147 | 148 | "source-map-resolve@^0.6.0": 149 | "integrity" "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==" 150 | "resolved" "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz" 151 | "version" "0.6.0" 152 | dependencies: 153 | "atob" "^2.1.2" 154 | "decode-uri-component" "^0.2.0" 155 | 156 | "tslib@^2.4.0": 157 | "integrity" "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 158 | "resolved" "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" 159 | "version" "2.6.2" 160 | -------------------------------------------------------------------------------- /dist/cjs/AnimatedCounter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var tslib_1 = require("tslib"); 4 | require("./styles.css"); 5 | var react_1 = tslib_1.__importStar(require("react")); 6 | var framer_motion_1 = require("framer-motion"); 7 | var util_1 = require("./util"); 8 | var hooks_1 = require("./hooks"); 9 | var debounce_1 = tslib_1.__importDefault(require("lodash/debounce")); 10 | // Array of digits to vertically scroll through 11 | var DIGIT_ARRAY = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; 12 | // Decimal element component 13 | var DecimalColumn = (0, react_1.memo)(function (_a) { 14 | var fontSize = _a.fontSize, color = _a.color, isComma = _a.isComma, digitStyles = _a.digitStyles; 15 | var decimalStyle = (0, react_1.useMemo)(function () { return (tslib_1.__assign({ fontSize: fontSize, lineHeight: fontSize, color: color, marginLeft: "calc(-".concat(fontSize, " / 10)") }, digitStyles)); }, [fontSize, color, digitStyles]); 16 | return (react_1["default"].createElement("span", { style: decimalStyle }, isComma ? ',' : '.')); 17 | }); 18 | // Individual number element component 19 | var NumberColumn = (0, react_1.memo)(function (_a) { 20 | var digit = _a.digit, delta = _a.delta, fontSize = _a.fontSize, color = _a.color, incrementColor = _a.incrementColor, decrementColor = _a.decrementColor, digitStyles = _a.digitStyles; 21 | var fontSizeValue = parseFloat(fontSize.replace('px', '')); 22 | var digitValue = parseInt(digit, 10); 23 | var _b = (0, react_1.useState)(fontSizeValue * digitValue), position = _b[0], setPosition = _b[1]; 24 | var _c = (0, react_1.useState)(null), animationClass = _c[0], setAnimationClass = _c[1]; 25 | var currentDigit = +digit; 26 | var previousDigit = (0, hooks_1.usePrevious)(+currentDigit); 27 | var columnContainer = (0, react_1.useRef)(null); 28 | var hasHydrated = (0, react_1.useRef)(false); 29 | var handleAnimationComplete = (0, react_1.useMemo)(function () { 30 | return (0, debounce_1["default"])(function () { 31 | setAnimationClass(""); 32 | }, 200); 33 | }, []); 34 | // Update the column position 35 | (0, react_1.useEffect)(function () { 36 | if (Number.isNaN(digitValue) || Number.isNaN(fontSizeValue)) { 37 | return; 38 | } 39 | // Each 'row' is assumed to be roughly one fontSize tall 40 | var newPosition = fontSizeValue * digitValue; 41 | setPosition(newPosition); 42 | }, [digitValue, fontSizeValue]); 43 | var containerStyle = (0, react_1.useMemo)(function () { return (tslib_1.__assign({ fontSize: fontSize, lineHeight: fontSize, height: 'auto', color: color, '--increment-color': "".concat(incrementColor), '--decrement-color': "".concat(decrementColor) }, digitStyles)); }, [fontSize, color, incrementColor, decrementColor, digitStyles]); 44 | var digitSpanStyle = (0, react_1.useMemo)(function () { return (tslib_1.__assign({ fontSize: fontSize, lineHeight: fontSize }, digitStyles)); }, [fontSize, digitStyles]); 45 | var negativeStyle = (0, react_1.useMemo)(function () { return (tslib_1.__assign({ color: color, fontSize: fontSize, lineHeight: fontSize, marginRight: "calc(".concat(fontSize, " / 5)") }, digitStyles)); }, [color, fontSize, digitStyles]); 46 | (0, react_1.useEffect)(function () { 47 | if (!hasHydrated.current) { 48 | hasHydrated.current = true; 49 | return; 50 | } 51 | setAnimationClass(previousDigit !== currentDigit ? delta : ''); 52 | }, [digit, delta]); 53 | // If digit is negative symbol, simply return an unanimated character 54 | if (digit === '-') { 55 | return (react_1["default"].createElement("span", { style: negativeStyle }, digit)); 56 | } 57 | return (react_1["default"].createElement("div", { className: 'ticker-column-container', ref: columnContainer, style: containerStyle }, 58 | react_1["default"].createElement(framer_motion_1.motion.div, tslib_1.__assign({ initial: { x: 0, y: position }, animate: { x: 0, y: position }, className: "ticker-column ".concat(animationClass), onAnimationComplete: handleAnimationComplete }, (!hasHydrated.current && { transition: { duration: 0 } })), DIGIT_ARRAY.map(function (num) { return (react_1["default"].createElement("div", { className: 'ticker-digit', key: num }, 59 | react_1["default"].createElement("span", { style: digitSpanStyle }, num))); })), 60 | react_1["default"].createElement("span", { className: 'number-placeholder' }, "0"))); 61 | }, function (prevProps, nextProps) { 62 | return (prevProps.digit === nextProps.digit && 63 | prevProps.delta === nextProps.delta && 64 | prevProps.fontSize === nextProps.fontSize && 65 | prevProps.color === nextProps.color && 66 | prevProps.incrementColor === nextProps.incrementColor && 67 | prevProps.decrementColor === nextProps.decrementColor && 68 | JSON.stringify(prevProps.digitStyles) === JSON.stringify(nextProps.digitStyles)); 69 | }); 70 | // Main component 71 | var AnimatedCounter = function (_a) { 72 | var _b = _a.value, value = _b === void 0 ? 0 : _b, _c = _a.fontSize, fontSize = _c === void 0 ? '18px' : _c, _d = _a.color, color = _d === void 0 ? 'black' : _d, _e = _a.incrementColor, incrementColor = _e === void 0 ? '#32cd32' : _e, _f = _a.decrementColor, decrementColor = _f === void 0 ? '#fe6862' : _f, _g = _a.includeDecimals, includeDecimals = _g === void 0 ? true : _g, _h = _a.decimalPrecision, decimalPrecision = _h === void 0 ? 2 : _h, _j = _a.includeCommas, includeCommas = _j === void 0 ? false : _j, _k = _a.containerStyles, containerStyles = _k === void 0 ? {} : _k, _l = _a.digitStyles, digitStyles = _l === void 0 ? {} : _l; 73 | var hasInitialRender = (0, react_1.useRef)(true); 74 | var numArray = (0, react_1.useMemo)(function () { 75 | return (0, util_1.formatForDisplay)(Math.abs(value), includeDecimals, decimalPrecision, includeCommas); 76 | }, [value, includeDecimals, decimalPrecision, includeCommas]); 77 | var previousNumber = (0, hooks_1.usePrevious)(value); 78 | var isNegative = value < 0; 79 | var delta = (0, react_1.useMemo)(function () { 80 | if (previousNumber !== null) { 81 | if (value > previousNumber) { 82 | return 'increase'; 83 | } 84 | else if (value < previousNumber) { 85 | return 'decrease'; 86 | } 87 | } 88 | return null; 89 | }, [value, previousNumber]); 90 | // Mark as hydrated after first render 91 | (0, react_1.useEffect)(function () { 92 | hasInitialRender.current = false; 93 | }, []); 94 | return (react_1["default"].createElement(framer_motion_1.motion.div, { className: 'ticker-view', style: containerStyles }, 95 | numArray.map(function (number, index) { 96 | return number === "." || number === "," ? (react_1["default"].createElement(DecimalColumn, { key: index, fontSize: fontSize, color: color, isComma: number === ",", digitStyles: digitStyles })) : (react_1["default"].createElement(NumberColumn, { key: index, digit: number, delta: delta, color: color, fontSize: fontSize, incrementColor: incrementColor, decrementColor: decrementColor, digitStyles: digitStyles })); 97 | }), 98 | isNegative && 99 | react_1["default"].createElement(NumberColumn, { key: 'negative-feedback', digit: '-', delta: delta, color: color, fontSize: fontSize, incrementColor: incrementColor, decrementColor: decrementColor, digitStyles: digitStyles }))); 100 | }; 101 | exports["default"] = react_1["default"].memo(AnimatedCounter); 102 | //# sourceMappingURL=AnimatedCounter.js.map -------------------------------------------------------------------------------- /src/AnimatedCounter.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | import React, { memo, useEffect, useRef, useState, CSSProperties, useMemo } from 'react'; 3 | import { motion } from "framer-motion"; 4 | import { formatForDisplay } from "./util"; 5 | import { usePrevious } from "./hooks"; 6 | import debounce from 'lodash/debounce'; 7 | 8 | export interface AnimatedCounterProps { 9 | value?: number; 10 | fontSize?: string; 11 | color?: string; 12 | incrementColor?: string; 13 | decrementColor?: string; 14 | includeDecimals?: boolean; 15 | decimalPrecision?: number; 16 | includeCommas?: boolean; 17 | containerStyles?: CSSProperties; 18 | digitStyles?: CSSProperties; 19 | } 20 | 21 | export interface NumberColumnProps { 22 | digit: string; 23 | delta: string | null; 24 | fontSize: string; 25 | color: string; 26 | incrementColor: string; 27 | decrementColor: string; 28 | digitStyles: CSSProperties; 29 | } 30 | 31 | export interface DecimalColumnProps { 32 | fontSize: string; 33 | color: string; 34 | isComma: boolean; 35 | digitStyles: CSSProperties; 36 | } 37 | 38 | // Array of digits to vertically scroll through 39 | const DIGIT_ARRAY = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; 40 | 41 | // Decimal element component 42 | const DecimalColumn = memo(({ fontSize, color, isComma, digitStyles }: DecimalColumnProps) => { 43 | const decimalStyle = useMemo(() => ({ 44 | fontSize: fontSize, 45 | lineHeight: fontSize, 46 | color: color, 47 | marginLeft: `calc(-${fontSize} / 10)`, 48 | ...digitStyles, 49 | }), [fontSize, color, digitStyles]); 50 | 51 | return ( 52 | 53 | {isComma ? ',' : '.'} 54 | 55 | ); 56 | }); 57 | 58 | // Individual number element component 59 | const NumberColumn = memo(({ 60 | digit, 61 | delta, 62 | fontSize, 63 | color, 64 | incrementColor, 65 | decrementColor, 66 | digitStyles, 67 | }: NumberColumnProps) => { 68 | 69 | const fontSizeValue = parseFloat(fontSize.replace('px', '')); 70 | const digitValue = parseInt(digit, 10); 71 | const [position, setPosition] = useState(fontSizeValue * digitValue); 72 | const [animationClass, setAnimationClass] = useState(null); 73 | const currentDigit = +digit; 74 | const previousDigit = usePrevious(+currentDigit); 75 | const columnContainer = useRef(null); 76 | const hasHydrated = useRef(false); 77 | 78 | const handleAnimationComplete = useMemo( 79 | () => 80 | debounce(() => { 81 | setAnimationClass(""); 82 | }, 200), 83 | [] 84 | ); 85 | 86 | // Update the column position 87 | useEffect(() => { 88 | if (Number.isNaN(digitValue) || Number.isNaN(fontSizeValue)) { 89 | return; 90 | } 91 | // Each 'row' is assumed to be roughly one fontSize tall 92 | const newPosition = fontSizeValue * digitValue; 93 | setPosition(newPosition); 94 | }, [digitValue, fontSizeValue]); 95 | 96 | const containerStyle = useMemo(() => ({ 97 | fontSize: fontSize, 98 | lineHeight: fontSize, 99 | height: 'auto' as const, 100 | color: color, 101 | '--increment-color': `${incrementColor}`, 102 | '--decrement-color': `${decrementColor}`, 103 | ...digitStyles, 104 | } as React.CSSProperties), [fontSize, color, incrementColor, decrementColor, digitStyles]); 105 | 106 | const digitSpanStyle = useMemo(() => ({ 107 | fontSize: fontSize, 108 | lineHeight: fontSize, 109 | ...digitStyles, 110 | }), [fontSize, digitStyles]); 111 | 112 | const negativeStyle = useMemo(() => ({ 113 | color: color, 114 | fontSize: fontSize, 115 | lineHeight: fontSize, 116 | marginRight: `calc(${fontSize} / 5)`, 117 | ...digitStyles 118 | }), [color, fontSize, digitStyles]); 119 | 120 | useEffect(() => { 121 | if (!hasHydrated.current) { 122 | hasHydrated.current = true; 123 | return; 124 | } 125 | setAnimationClass(previousDigit !== currentDigit ? delta : ''); 126 | }, [digit, delta]); 127 | 128 | // If digit is negative symbol, simply return an unanimated character 129 | if (digit === '-') { 130 | return ( 131 | 132 | {digit} 133 | 134 | ) 135 | } 136 | 137 | return ( 138 |
143 | 150 | {DIGIT_ARRAY.map((num) => ( 151 |
152 | 153 | {num} 154 | 155 |
156 | ))} 157 |
158 | 0 159 |
160 | ); 161 | }, (prevProps, nextProps) => { 162 | return ( 163 | prevProps.digit === nextProps.digit && 164 | prevProps.delta === nextProps.delta && 165 | prevProps.fontSize === nextProps.fontSize && 166 | prevProps.color === nextProps.color && 167 | prevProps.incrementColor === nextProps.incrementColor && 168 | prevProps.decrementColor === nextProps.decrementColor && 169 | JSON.stringify(prevProps.digitStyles) === JSON.stringify(nextProps.digitStyles) 170 | ); 171 | }); 172 | 173 | // Main component 174 | const AnimatedCounter = ({ 175 | value = 0, 176 | fontSize = '18px', 177 | color = 'black', 178 | incrementColor = '#32cd32', 179 | decrementColor = '#fe6862', 180 | includeDecimals = true, 181 | decimalPrecision = 2, 182 | includeCommas = false, 183 | containerStyles = {}, 184 | digitStyles = {}, 185 | }: AnimatedCounterProps) => { 186 | 187 | const hasInitialRender = useRef(true); 188 | 189 | const numArray = useMemo(() => 190 | formatForDisplay(Math.abs(value), includeDecimals, decimalPrecision, includeCommas), 191 | [value, includeDecimals, decimalPrecision, includeCommas] 192 | ); 193 | 194 | const previousNumber = usePrevious(value); 195 | const isNegative = value < 0; 196 | 197 | const delta = useMemo((): string | null => { 198 | if (previousNumber !== null) { 199 | if (value > previousNumber) { 200 | return 'increase'; 201 | } else if (value < previousNumber) { 202 | return 'decrease'; 203 | } 204 | } 205 | return null; 206 | }, [value, previousNumber]); 207 | 208 | // Mark as hydrated after first render 209 | useEffect(() => { 210 | hasInitialRender.current = false; 211 | }, []); 212 | 213 | return ( 214 | 218 | {/* Format integer to NumberColumn components */} 219 | {numArray.map((number: string, index: number) => 220 | number === "." || number === "," ? ( 221 | 228 | ) : ( 229 | 239 | ) 240 | )} 241 | {/* If number is negative, render '-' feedback */} 242 | {isNegative && 243 | 253 | } 254 | 255 | ); 256 | } 257 | 258 | export default React.memo(AnimatedCounter); --------------------------------------------------------------------------------