@waveso/ui 0.0.7 → 0.0.9

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/dist/count.js ADDED
@@ -0,0 +1,176 @@
1
+ import { useRef, useState, useEffect, isValidElement, cloneElement } from 'react';
2
+ import { useInView } from 'motion/react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ function easeOut(t) {
6
+ return 1 - Math.pow(1 - t, 3);
7
+ }
8
+ function formatCountdown(ms) {
9
+ if (ms <= 0) return "00:00:00";
10
+ const totalSeconds = Math.floor(ms / 1e3);
11
+ const days = Math.floor(totalSeconds / 86400);
12
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
13
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
14
+ const seconds = totalSeconds % 60;
15
+ const pad = (n) => String(n).padStart(2, "0");
16
+ if (days > 0) {
17
+ return `${days}d ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
18
+ }
19
+ return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
20
+ }
21
+ function mergeRef(internalRef, externalRef) {
22
+ return (el) => {
23
+ internalRef.current = el;
24
+ if (typeof externalRef === "function") externalRef(el);
25
+ else if (externalRef && typeof externalRef === "object") {
26
+ externalRef.current = el;
27
+ }
28
+ };
29
+ }
30
+ function Count({
31
+ to,
32
+ from: start = 0,
33
+ duration = 900,
34
+ delay = 0,
35
+ format,
36
+ prefix = "",
37
+ suffix = "",
38
+ children,
39
+ once = true,
40
+ easing = easeOut,
41
+ onComplete
42
+ }) {
43
+ const isDate = to instanceof Date;
44
+ if (isDate) {
45
+ return /* @__PURE__ */ jsx(
46
+ DateCount,
47
+ {
48
+ to,
49
+ delay,
50
+ format,
51
+ prefix,
52
+ suffix,
53
+ once,
54
+ onComplete,
55
+ children
56
+ }
57
+ );
58
+ }
59
+ return /* @__PURE__ */ jsx(
60
+ NumberCount,
61
+ {
62
+ to,
63
+ from: start,
64
+ duration,
65
+ delay,
66
+ format,
67
+ prefix,
68
+ suffix,
69
+ once,
70
+ easing,
71
+ onComplete,
72
+ children
73
+ }
74
+ );
75
+ }
76
+ function NumberCount({
77
+ to,
78
+ from: start,
79
+ duration,
80
+ delay,
81
+ format,
82
+ prefix,
83
+ suffix,
84
+ children,
85
+ once,
86
+ easing,
87
+ onComplete
88
+ }) {
89
+ const ref = useRef(null);
90
+ const isInView = useInView(ref, { once, margin: "-50px" });
91
+ const [display, setDisplay] = useState(start);
92
+ const hasAnimated = useRef(false);
93
+ const formatFn = format ?? ((n) => Number.isInteger(to) ? Math.round(n).toLocaleString() : n.toLocaleString());
94
+ useEffect(() => {
95
+ if (!isInView || hasAnimated.current) return;
96
+ hasAnimated.current = true;
97
+ const delayMs = delay * 1e3;
98
+ let raf;
99
+ let startTime;
100
+ const timer = setTimeout(() => {
101
+ const animate = (timestamp) => {
102
+ if (!startTime) startTime = timestamp;
103
+ const elapsed = timestamp - startTime;
104
+ const progress = Math.min(elapsed / duration, 1);
105
+ const easedProgress = easing(progress);
106
+ const current = start + (to - start) * easedProgress;
107
+ setDisplay(current);
108
+ if (progress < 1) {
109
+ raf = requestAnimationFrame(animate);
110
+ } else {
111
+ onComplete?.();
112
+ }
113
+ };
114
+ raf = requestAnimationFrame(animate);
115
+ }, delayMs);
116
+ return () => {
117
+ clearTimeout(timer);
118
+ cancelAnimationFrame(raf);
119
+ };
120
+ }, [isInView, to, start, duration, delay, easing, onComplete]);
121
+ if (!isValidElement(children)) return children;
122
+ const childProps = children.props;
123
+ const existingRef = childProps.ref;
124
+ return cloneElement(children, {
125
+ ref: mergeRef(ref, existingRef),
126
+ children: `${prefix}${formatFn(display)}${suffix}`
127
+ });
128
+ }
129
+ function DateCount({
130
+ to,
131
+ delay,
132
+ format,
133
+ prefix,
134
+ suffix,
135
+ children,
136
+ once,
137
+ onComplete
138
+ }) {
139
+ const ref = useRef(null);
140
+ const isInView = useInView(ref, { once, margin: "-50px" });
141
+ const [remaining, setRemaining] = useState(() => Math.max(0, to.getTime() - Date.now()));
142
+ const [started, setStarted] = useState(false);
143
+ const completedRef = useRef(false);
144
+ const formatFn = format ?? formatCountdown;
145
+ useEffect(() => {
146
+ if (!isInView || started) return;
147
+ const timer = setTimeout(() => setStarted(true), delay * 1e3);
148
+ return () => clearTimeout(timer);
149
+ }, [isInView, delay, started]);
150
+ useEffect(() => {
151
+ if (!started) return;
152
+ const tick = () => {
153
+ const ms = Math.max(0, to.getTime() - Date.now());
154
+ setRemaining(ms);
155
+ if (ms <= 0 && !completedRef.current) {
156
+ completedRef.current = true;
157
+ onComplete?.();
158
+ }
159
+ };
160
+ tick();
161
+ const interval = setInterval(tick, 1e3);
162
+ return () => clearInterval(interval);
163
+ }, [started, to, onComplete]);
164
+ if (!isValidElement(children)) return children;
165
+ const childProps = children.props;
166
+ const existingRef = childProps.ref;
167
+ return cloneElement(children, {
168
+ ref: mergeRef(ref, existingRef),
169
+ children: `${prefix}${formatFn(remaining)}${suffix}`
170
+ });
171
+ }
172
+ var CountUp = Count;
173
+
174
+ export { Count, CountUp, easeOut };
175
+ //# sourceMappingURL=count.js.map
176
+ //# sourceMappingURL=count.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/count.tsx"],"names":[],"mappings":";;;;AAmDA,SAAS,QAAQ,CAAA,EAAmB;AAClC,EAAA,OAAO,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,GAAG,CAAC,CAAA;AAC9B;AAIA,SAAS,gBAAgB,EAAA,EAAoB;AAC3C,EAAA,IAAI,EAAA,IAAM,GAAG,OAAO,UAAA;AAEpB,EAAA,MAAM,YAAA,GAAe,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,GAAI,CAAA;AACzC,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,YAAA,GAAe,KAAK,CAAA;AAC5C,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAO,YAAA,GAAe,QAAS,IAAI,CAAA;AACtD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAO,YAAA,GAAe,OAAQ,EAAE,CAAA;AACrD,EAAA,MAAM,UAAU,YAAA,GAAe,EAAA;AAE/B,EAAA,MAAM,GAAA,GAAM,CAAC,CAAA,KAAc,MAAA,CAAO,CAAC,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AAEpD,EAAA,IAAI,OAAO,CAAA,EAAG;AACZ,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAA,EAAK,GAAA,CAAI,KAAK,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,OAAO,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,OAAO,CAAC,CAAA,CAAA;AAAA,EAC/D;AACA,EAAA,OAAO,CAAA,EAAG,GAAA,CAAI,KAAK,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,OAAO,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,OAAO,CAAC,CAAA,CAAA;AACtD;AAIA,SAAS,QAAA,CACP,aACA,WAAA,EACA;AACA,EAAA,OAAO,CAAC,EAAA,KAA2B;AAChC,IAAC,YAAgD,OAAA,GAAU,EAAA;AAC5D,IAAA,IAAI,OAAO,WAAA,KAAgB,UAAA,EAAY,WAAA,CAAY,EAAE,CAAA;AAAA,SAAA,IAC5C,WAAA,IAAe,OAAO,WAAA,KAAgB,QAAA,EAAU;AACtD,MAAC,YAAgD,OAAA,GAAU,EAAA;AAAA,IAC9D;AAAA,EACF,CAAA;AACF;AAoCA,SAAS,KAAA,CAAM;AAAA,EACb,EAAA;AAAA,EACA,MAAM,KAAA,GAAQ,CAAA;AAAA,EACd,QAAA,GAAW,GAAA;AAAA,EACX,KAAA,GAAQ,CAAA;AAAA,EACR,MAAA;AAAA,EACA,MAAA,GAAS,EAAA;AAAA,EACT,MAAA,GAAS,EAAA;AAAA,EACT,QAAA;AAAA,EACA,IAAA,GAAO,IAAA;AAAA,EACP,MAAA,GAAS,OAAA;AAAA,EACT;AACF,CAAA,EAAe;AACb,EAAA,MAAM,SAAS,EAAA,YAAc,IAAA;AAE7B,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,uBACE,GAAA;AAAA,MAAC,SAAA;AAAA,MAAA;AAAA,QACC,EAAA;AAAA,QACA,KAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,IAAA;AAAA,QACA,UAAA;AAAA,QAEC;AAAA;AAAA,KACH;AAAA,EAEJ;AAEA,EAAA,uBACE,GAAA;AAAA,IAAC,WAAA;AAAA,IAAA;AAAA,MACC,EAAA;AAAA,MACA,IAAA,EAAM,KAAA;AAAA,MACN,QAAA;AAAA,MACA,KAAA;AAAA,MACA,MAAA;AAAA,MACA,MAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAA;AAAA,MACA,MAAA;AAAA,MACA,UAAA;AAAA,MAEC;AAAA;AAAA,GACH;AAEJ;AAIA,SAAS,WAAA,CAAY;AAAA,EACnB,EAAA;AAAA,EACA,IAAA,EAAM,KAAA;AAAA,EACN,QAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,IAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,EAYG;AACD,EAAA,MAAM,GAAA,GAAM,OAAoB,IAAI,CAAA;AACpC,EAAA,MAAM,WAAW,SAAA,CAAU,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AACzD,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,EAAA,MAAM,WAAA,GAAc,OAAO,KAAK,CAAA;AAEhC,EAAA,MAAM,QAAA,GAAW,MAAA,KAAW,CAAC,CAAA,KAC3B,OAAO,SAAA,CAAU,EAAE,CAAA,GAAI,IAAA,CAAK,MAAM,CAAC,CAAA,CAAE,cAAA,EAAe,GAAI,EAAE,cAAA,EAAe,CAAA;AAG3E,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,QAAA,IAAY,WAAA,CAAY,OAAA,EAAS;AACtC,IAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AAEtB,IAAA,MAAM,UAAU,KAAA,GAAQ,GAAA;AACxB,IAAA,IAAI,GAAA;AACJ,IAAA,IAAI,SAAA;AAEJ,IAAA,MAAM,KAAA,GAAQ,WAAW,MAAM;AAC7B,MAAA,MAAM,OAAA,GAAU,CAAC,SAAA,KAAsB;AACrC,QAAA,IAAI,CAAC,WAAW,SAAA,GAAY,SAAA;AAC5B,QAAA,MAAM,UAAU,SAAA,GAAY,SAAA;AAC5B,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,OAAA,GAAU,UAAU,CAAC,CAAA;AAC/C,QAAA,MAAM,aAAA,GAAgB,OAAO,QAAQ,CAAA;AACrC,QAAA,MAAM,OAAA,GAAU,KAAA,GAAA,CAAS,EAAA,GAAK,KAAA,IAAS,aAAA;AAEvC,QAAA,UAAA,CAAW,OAAO,CAAA;AAElB,QAAA,IAAI,WAAW,CAAA,EAAG;AAChB,UAAA,GAAA,GAAM,sBAAsB,OAAO,CAAA;AAAA,QACrC,CAAA,MAAO;AACL,UAAA,UAAA,IAAa;AAAA,QACf;AAAA,MACF,CAAA;AAEA,MAAA,GAAA,GAAM,sBAAsB,OAAO,CAAA;AAAA,IACrC,GAAG,OAAO,CAAA;AAEV,IAAA,OAAO,MAAM;AACX,MAAA,YAAA,CAAa,KAAK,CAAA;AAClB,MAAA,oBAAA,CAAqB,GAAG,CAAA;AAAA,IAC1B,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,QAAA,EAAU,EAAA,EAAI,OAAO,QAAA,EAAU,KAAA,EAAO,MAAA,EAAQ,UAAU,CAAC,CAAA;AAE7D,EAAA,IAAI,CAAC,cAAA,CAAe,QAAQ,CAAA,EAAG,OAAO,QAAA;AAEtC,EAAA,MAAM,aAAa,QAAA,CAAS,KAAA;AAC5B,EAAA,MAAM,cAAe,UAAA,CAA0C,GAAA;AAE/D,EAAA,OAAO,aAAa,QAAA,EAAU;AAAA,IAC5B,GAAA,EAAK,QAAA,CAAS,GAAA,EAAK,WAAW,CAAA;AAAA,IAC9B,QAAA,EAAU,GAAG,MAAM,CAAA,EAAG,SAAS,OAAO,CAAC,GAAG,MAAM,CAAA;AAAA,GACtB,CAAA;AAC9B;AAIA,SAAS,SAAA,CAAU;AAAA,EACjB,EAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,IAAA;AAAA,EACA;AACF,CAAA,EASG;AACD,EAAA,MAAM,GAAA,GAAM,OAAoB,IAAI,CAAA;AACpC,EAAA,MAAM,WAAW,SAAA,CAAU,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AACzD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,MAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAG,OAAA,EAAQ,GAAI,IAAA,CAAK,GAAA,EAAK,CAAC,CAAA;AACvF,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,EAAA,MAAM,YAAA,GAAe,OAAO,KAAK,CAAA;AAEjC,EAAA,MAAM,WAAW,MAAA,IAAU,eAAA;AAE3B,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,YAAY,OAAA,EAAS;AAC1B,IAAA,MAAM,QAAQ,UAAA,CAAW,MAAM,WAAW,IAAI,CAAA,EAAG,QAAQ,GAAI,CAAA;AAC7D,IAAA,OAAO,MAAM,aAAa,KAAK,CAAA;AAAA,EACjC,CAAA,EAAG,CAAC,QAAA,EAAU,KAAA,EAAO,OAAO,CAAC,CAAA;AAE7B,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,MAAM,OAAO,MAAM;AACjB,MAAA,MAAM,EAAA,GAAK,KAAK,GAAA,CAAI,CAAA,EAAG,GAAG,OAAA,EAAQ,GAAI,IAAA,CAAK,GAAA,EAAK,CAAA;AAChD,MAAA,YAAA,CAAa,EAAE,CAAA;AAEf,MAAA,IAAI,EAAA,IAAM,CAAA,IAAK,CAAC,YAAA,CAAa,OAAA,EAAS;AACpC,QAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AACvB,QAAA,UAAA,IAAa;AAAA,MACf;AAAA,IACF,CAAA;AAEA,IAAA,IAAA,EAAK;AACL,IAAA,MAAM,QAAA,GAAW,WAAA,CAAY,IAAA,EAAM,GAAI,CAAA;AACvC,IAAA,OAAO,MAAM,cAAc,QAAQ,CAAA;AAAA,EACrC,CAAA,EAAG,CAAC,OAAA,EAAS,EAAA,EAAI,UAAU,CAAC,CAAA;AAE5B,EAAA,IAAI,CAAC,cAAA,CAAe,QAAQ,CAAA,EAAG,OAAO,QAAA;AAEtC,EAAA,MAAM,aAAa,QAAA,CAAS,KAAA;AAC5B,EAAA,MAAM,cAAe,UAAA,CAA0C,GAAA;AAE/D,EAAA,OAAO,aAAa,QAAA,EAAU;AAAA,IAC5B,GAAA,EAAK,QAAA,CAAS,GAAA,EAAK,WAAW,CAAA;AAAA,IAC9B,QAAA,EAAU,GAAG,MAAM,CAAA,EAAG,SAAS,SAAS,CAAC,GAAG,MAAM,CAAA;AAAA,GACxB,CAAA;AAC9B;AAKA,IAAM,OAAA,GAAU","file":"count.js","sourcesContent":["\"use client\"\n\nimport {\n type ReactElement,\n type Ref,\n cloneElement,\n isValidElement,\n useEffect,\n useRef,\n useState,\n} from \"react\"\nimport { useInView } from \"motion/react\"\n\n// ── Types ────────────────────────────────────────────────────────────\n\ninterface CountProps {\n /** Target number or Date to count to */\n to: number | Date\n /** Starting number. Default: 0. Ignored when `to` is a Date. */\n from?: number\n /** Animation duration in milliseconds. Default: 900. Ignored when `to` is a Date. */\n duration?: number\n /** Delay before starting in seconds. Default: 0 */\n delay?: number\n /**\n * Format the value for display.\n * - For numbers: receives the current interpolated number.\n * - For dates: receives remaining milliseconds.\n * Default: toLocaleString() for numbers, dd:hh:mm:ss for dates.\n */\n format?: (value: number) => string\n /** Prefix string (e.g., \"$\"). Default: '' */\n prefix?: string\n /** Suffix string (e.g., \"%\", \"+\"). Default: '' */\n suffix?: string\n /** Element to render into. Receives the formatted value as children. */\n children: ReactElement\n /** Trigger once. Default: true */\n once?: boolean\n /** Easing function. Default: easeOut. Ignored when `to` is a Date. */\n easing?: (t: number) => number\n /** Called when the count finishes (reaches target or date passes). */\n onComplete?: () => void\n}\n\n/** @deprecated Use `Count` instead. `CountUp` is an alias kept for backwards compatibility. */\ntype CountUpProps = CountProps\n\n// ── Easing ───────────────────────────────────────────────────────────\n\n/** Cubic ease-out: fast start, smooth deceleration */\nfunction easeOut(t: number): number {\n return 1 - Math.pow(1 - t, 3)\n}\n\n// ── Date Formatting ──────────────────────────────────────────────────\n\nfunction formatCountdown(ms: number): string {\n if (ms <= 0) return \"00:00:00\"\n\n const totalSeconds = Math.floor(ms / 1000)\n const days = Math.floor(totalSeconds / 86400)\n const hours = Math.floor((totalSeconds % 86400) / 3600)\n const minutes = Math.floor((totalSeconds % 3600) / 60)\n const seconds = totalSeconds % 60\n\n const pad = (n: number) => String(n).padStart(2, \"0\")\n\n if (days > 0) {\n return `${days}d ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`\n }\n return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`\n}\n\n// ── Ref Merge ────────────────────────────────────────────────────────\n\nfunction mergeRef(\n internalRef: React.RefObject<HTMLElement | null>,\n externalRef?: Ref<HTMLElement>,\n) {\n return (el: HTMLElement | null) => {\n ;(internalRef as { current: HTMLElement | null }).current = el\n if (typeof externalRef === \"function\") externalRef(el)\n else if (externalRef && typeof externalRef === \"object\") {\n ;(externalRef as { current: HTMLElement | null }).current = el\n }\n }\n}\n\n// ── Count ────────────────────────────────────────────────────────────\n\n/**\n * Animated number counter. Counts up, counts down, or live-counts to a date.\n *\n * Direction is automatic — if `from < to` it counts up, if `from > to` it\n * counts down. When `to` is a Date, it becomes a live countdown that ticks\n * every second.\n *\n * Zero wrapper — injects the formatted value as children via cloneElement.\n *\n * @example\n * ```tsx\n * // Count up\n * <Count to={1234}>\n * <span className=\"text-4xl font-bold tabular-nums\" />\n * </Count>\n *\n * // Count down\n * <Count from={100} to={0} onComplete={() => alert(\"Done!\")}>\n * <span className=\"text-4xl font-bold tabular-nums\" />\n * </Count>\n *\n * // Live countdown to a date\n * <Count to={new Date(\"2026-04-01T00:00:00\")}>\n * <span className=\"text-2xl font-mono tabular-nums\" />\n * </Count>\n *\n * // Custom date format\n * <Count to={launchDate} format={(ms) => `${Math.ceil(ms / 86400000)} days left`}>\n * <span className=\"text-xl\" />\n * </Count>\n * ```\n */\nfunction Count({\n to,\n from: start = 0,\n duration = 900,\n delay = 0,\n format,\n prefix = \"\",\n suffix = \"\",\n children,\n once = true,\n easing = easeOut,\n onComplete,\n}: CountProps) {\n const isDate = to instanceof Date\n\n if (isDate) {\n return (\n <DateCount\n to={to}\n delay={delay}\n format={format}\n prefix={prefix}\n suffix={suffix}\n once={once}\n onComplete={onComplete}\n >\n {children}\n </DateCount>\n )\n }\n\n return (\n <NumberCount\n to={to}\n from={start}\n duration={duration}\n delay={delay}\n format={format}\n prefix={prefix}\n suffix={suffix}\n once={once}\n easing={easing}\n onComplete={onComplete}\n >\n {children}\n </NumberCount>\n )\n}\n\n// ── Number Count (up or down) ────────────────────────────────────────\n\nfunction NumberCount({\n to,\n from: start,\n duration,\n delay,\n format,\n prefix,\n suffix,\n children,\n once,\n easing,\n onComplete,\n}: {\n to: number\n from: number\n duration: number\n delay: number\n format?: (value: number) => string\n prefix: string\n suffix: string\n children: ReactElement\n once: boolean\n easing: (t: number) => number\n onComplete?: () => void\n}) {\n const ref = useRef<HTMLElement>(null)\n const isInView = useInView(ref, { once, margin: \"-50px\" })\n const [display, setDisplay] = useState(start)\n const hasAnimated = useRef(false)\n\n const formatFn = format ?? ((n: number) =>\n Number.isInteger(to) ? Math.round(n).toLocaleString() : n.toLocaleString()\n )\n\n useEffect(() => {\n if (!isInView || hasAnimated.current) return\n hasAnimated.current = true\n\n const delayMs = delay * 1000\n let raf: number\n let startTime: number\n\n const timer = setTimeout(() => {\n const animate = (timestamp: number) => {\n if (!startTime) startTime = timestamp\n const elapsed = timestamp - startTime\n const progress = Math.min(elapsed / duration, 1)\n const easedProgress = easing(progress)\n const current = start + (to - start) * easedProgress\n\n setDisplay(current)\n\n if (progress < 1) {\n raf = requestAnimationFrame(animate)\n } else {\n onComplete?.()\n }\n }\n\n raf = requestAnimationFrame(animate)\n }, delayMs)\n\n return () => {\n clearTimeout(timer)\n cancelAnimationFrame(raf)\n }\n }, [isInView, to, start, duration, delay, easing, onComplete])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n return cloneElement(children, {\n ref: mergeRef(ref, existingRef),\n children: `${prefix}${formatFn(display)}${suffix}`,\n } as Record<string, unknown>)\n}\n\n// ── Date Count (live countdown) ──────────────────────────────────────\n\nfunction DateCount({\n to,\n delay,\n format,\n prefix,\n suffix,\n children,\n once,\n onComplete,\n}: {\n to: Date\n delay: number\n format?: (value: number) => string\n prefix: string\n suffix: string\n children: ReactElement\n once: boolean\n onComplete?: () => void\n}) {\n const ref = useRef<HTMLElement>(null)\n const isInView = useInView(ref, { once, margin: \"-50px\" })\n const [remaining, setRemaining] = useState(() => Math.max(0, to.getTime() - Date.now()))\n const [started, setStarted] = useState(false)\n const completedRef = useRef(false)\n\n const formatFn = format ?? formatCountdown\n\n useEffect(() => {\n if (!isInView || started) return\n const timer = setTimeout(() => setStarted(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [isInView, delay, started])\n\n useEffect(() => {\n if (!started) return\n\n const tick = () => {\n const ms = Math.max(0, to.getTime() - Date.now())\n setRemaining(ms)\n\n if (ms <= 0 && !completedRef.current) {\n completedRef.current = true\n onComplete?.()\n }\n }\n\n tick()\n const interval = setInterval(tick, 1000)\n return () => clearInterval(interval)\n }, [started, to, onComplete])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n return cloneElement(children, {\n ref: mergeRef(ref, existingRef),\n children: `${prefix}${formatFn(remaining)}${suffix}`,\n } as Record<string, unknown>)\n}\n\n// ── Exports ──────────────────────────────────────────────────────────\n\n/** @deprecated Use `Count` instead */\nconst CountUp = Count\n\nexport { Count, CountUp, easeOut }\nexport type { CountProps, CountUpProps }\n"]}
package/dist/dialog.js CHANGED
@@ -1,5 +1,5 @@
1
- import { Button } from './chunk-OUFYQLVN.js';
2
1
  import { CloseIcon } from './chunk-DIGOLJIR.js';
2
+ import { Button } from './chunk-OUFYQLVN.js';
3
3
  import { cn } from './chunk-76UQO56T.js';
4
4
  import { Dialog as Dialog$1 } from '@base-ui/react/dialog';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
package/dist/drawer.js CHANGED
@@ -1,6 +1,6 @@
1
- export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger } from './chunk-MO4KRZFJ.js';
2
- import './chunk-OUFYQLVN.js';
1
+ export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger } from './chunk-BKTJYX4M.js';
3
2
  import './chunk-DIGOLJIR.js';
3
+ import './chunk-OUFYQLVN.js';
4
4
  import './chunk-76UQO56T.js';
5
5
  //# sourceMappingURL=drawer.js.map
6
6
  //# sourceMappingURL=drawer.js.map
@@ -0,0 +1,15 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+
4
+ interface FilmGrainProps {
5
+ density?: number;
6
+ opacity?: number;
7
+ blendMode?: React.CSSProperties["mixBlendMode"];
8
+ fps?: number;
9
+ color?: string;
10
+ className?: string;
11
+ style?: React.CSSProperties;
12
+ }
13
+ declare function FilmGrain({ density, opacity, blendMode, fps, color, className, style, }: FilmGrainProps): react_jsx_runtime.JSX.Element;
14
+
15
+ export { FilmGrain, type FilmGrainProps };
@@ -0,0 +1,422 @@
1
+ import { useRef, useMemo, useEffect } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/film-grain-shader.ts
5
+ var vertexShader = `
6
+ attribute vec2 position;
7
+ varying vec2 vUv;
8
+
9
+ void main() {
10
+ vUv = position * 0.5 + 0.5;
11
+ gl_Position = vec4(position, 0.0, 1.0);
12
+ }
13
+ `;
14
+ var fragmentShader = `
15
+ precision highp float;
16
+
17
+ uniform float uTime;
18
+ uniform vec2 uResolution;
19
+ uniform float uDensity;
20
+ uniform float uOpacity;
21
+ uniform float uFps;
22
+ uniform vec3 uColor;
23
+
24
+ varying vec2 vUv;
25
+
26
+ // Stable hash
27
+ float hash(vec2 p, float seed) {
28
+ return fract(sin(dot(p + seed, vec2(127.1, 311.7))) * 43758.5453123);
29
+ }
30
+
31
+ // Film response curve (soft toe + shoulder rolloff)
32
+ float filmCurve(float x) {
33
+ return smoothstep(0.0, 0.2, x) * (1.0 - smoothstep(0.8, 1.0, x));
34
+ }
35
+
36
+ void main() {
37
+ vec2 uv = gl_FragCoord.xy;
38
+ vec2 p = floor(uv);
39
+
40
+ // Temporal coherence: blend between current and next frame seed
41
+ float frame = floor(uTime * uFps);
42
+ float t = fract(uTime * uFps);
43
+ float seed = frame * 0.1731;
44
+
45
+ // Multi-scale grain with inter-frame blending on fine layer
46
+ float fineA = hash(p, seed);
47
+ float fineB = hash(p, seed + 0.1731);
48
+ float fine = mix(fineA, fineB, t);
49
+
50
+ float medium = hash(floor(uv * 0.5), seed + 3.1);
51
+ float coarse = hash(floor(uv * 0.25), seed + 7.93);
52
+
53
+ float grain = fine * 0.65 + medium * 0.25 + coarse * 0.10;
54
+
55
+ // Emulsion clumping (organic structure)
56
+ float cluster = hash(floor(uv * 0.18), seed + 9.2);
57
+ grain *= mix(0.75, 1.35, cluster);
58
+
59
+ // Density threshold
60
+ grain = smoothstep(1.0 - uDensity, 1.0, grain);
61
+
62
+ // Film response curve (replaces raw pow gamma)
63
+ grain = filmCurve(grain);
64
+
65
+ // Micro-blur: sample neighbors for optical diffusion
66
+ float n1 = hash(p + vec2(1.0, 0.0), seed);
67
+ float n2 = hash(p - vec2(1.0, 0.0), seed);
68
+ float n3 = hash(p + vec2(0.0, 1.0), seed);
69
+ float n4 = hash(p - vec2(0.0, 1.0), seed);
70
+ float neighbors = (n1 + n2 + n3 + n4) * 0.25;
71
+ grain = mix(grain, neighbors, 0.15);
72
+
73
+ // Chromatic aberration (reuse neighbor hashes)
74
+ float rShift = n1 * 0.015;
75
+ float bShift = n2 * 0.015;
76
+
77
+ vec3 color = uColor * vec3(
78
+ grain + rShift,
79
+ grain,
80
+ grain + bShift
81
+ );
82
+
83
+ color = clamp(color, 0.0, 1.0);
84
+
85
+ gl_FragColor = vec4(color, grain * uOpacity);
86
+ }
87
+ `;
88
+
89
+ // src/film-grain-webgl.ts
90
+ function parseHex(hex) {
91
+ if (!/^#([0-9a-f]{3,8})$/i.test(hex)) return [255, 255, 255, 255];
92
+ let h = hex.replace("#", "");
93
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
94
+ if (h.length === 6) h += "ff";
95
+ const n = parseInt(h, 16);
96
+ return [n >> 24 & 255, n >> 16 & 255, n >> 8 & 255, n & 255];
97
+ }
98
+ function initWebGL(gl) {
99
+ const createShader = (type, source) => {
100
+ const shader = gl.createShader(type);
101
+ if (!shader) return null;
102
+ gl.shaderSource(shader, source);
103
+ gl.compileShader(shader);
104
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
105
+ gl.deleteShader(shader);
106
+ return null;
107
+ }
108
+ return shader;
109
+ };
110
+ const vs = createShader(gl.VERTEX_SHADER, vertexShader);
111
+ const fs = createShader(gl.FRAGMENT_SHADER, fragmentShader);
112
+ if (!vs || !fs) return null;
113
+ const program = gl.createProgram();
114
+ if (!program) return null;
115
+ gl.attachShader(program, vs);
116
+ gl.attachShader(program, fs);
117
+ gl.linkProgram(program);
118
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
119
+ gl.deleteProgram(program);
120
+ return null;
121
+ }
122
+ gl.useProgram(program);
123
+ const buffer = gl.createBuffer();
124
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
125
+ gl.bufferData(
126
+ gl.ARRAY_BUFFER,
127
+ new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
128
+ gl.STATIC_DRAW
129
+ );
130
+ const position = gl.getAttribLocation(program, "position");
131
+ gl.enableVertexAttribArray(position);
132
+ gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
133
+ return {
134
+ uTime: gl.getUniformLocation(program, "uTime"),
135
+ uResolution: gl.getUniformLocation(program, "uResolution"),
136
+ uDensity: gl.getUniformLocation(program, "uDensity"),
137
+ uOpacity: gl.getUniformLocation(program, "uOpacity"),
138
+ uFps: gl.getUniformLocation(program, "uFps"),
139
+ uColor: gl.getUniformLocation(program, "uColor"),
140
+ program,
141
+ vs,
142
+ fs,
143
+ buffer
144
+ };
145
+ }
146
+ function setUniform1f(gl, loc, val) {
147
+ if (loc) gl.uniform1f(loc, val);
148
+ }
149
+ function setUniform2f(gl, loc, x, y) {
150
+ if (loc) gl.uniform2f(loc, x, y);
151
+ }
152
+ function setUniform3f(gl, loc, x, y, z) {
153
+ if (loc) gl.uniform3f(loc, x, y, z);
154
+ }
155
+ function generateGrainTexture(width, height, density, color) {
156
+ const offscreen = document.createElement("canvas");
157
+ offscreen.width = width;
158
+ offscreen.height = height;
159
+ const ctx = offscreen.getContext("2d");
160
+ if (!ctx) return offscreen;
161
+ const [r, g, b, a] = parseHex(color);
162
+ const imageData = ctx.createImageData(width, height);
163
+ const pixels = new Uint32Array(imageData.data.buffer);
164
+ for (let i = 0; i < pixels.length; i++) {
165
+ if (Math.random() < density) {
166
+ const variation = Math.round(a * (0.4 + Math.random() * 0.6));
167
+ pixels[i] = (variation << 24 | b << 16 | g << 8 | r) >>> 0;
168
+ }
169
+ }
170
+ ctx.putImageData(imageData, 0, 0);
171
+ return offscreen;
172
+ }
173
+ function useFilmGrain({
174
+ density,
175
+ opacity,
176
+ fps = 18,
177
+ color = "#ffffff"
178
+ }) {
179
+ const canvasRef = useRef(null);
180
+ const rafRef = useRef(0);
181
+ const visibleRef = useRef(true);
182
+ const grainTextureRef = useRef(null);
183
+ const resizeTimerRef = useRef(null);
184
+ const interval = useMemo(() => 1e3 / fps, [fps]);
185
+ useEffect(() => {
186
+ const canvas = canvasRef.current;
187
+ if (!canvas) return;
188
+ const container = canvas.parentElement;
189
+ if (!container) return;
190
+ const prefersReducedMotion = window.matchMedia(
191
+ "(prefers-reduced-motion: reduce)"
192
+ ).matches;
193
+ const intersectionObs = new IntersectionObserver(
194
+ (entries) => {
195
+ const entry = entries[0];
196
+ if (entry) visibleRef.current = entry.isIntersecting;
197
+ },
198
+ { threshold: 0.01 }
199
+ );
200
+ intersectionObs.observe(container);
201
+ const getDimensions = () => {
202
+ const dpr = window.devicePixelRatio || 1;
203
+ return {
204
+ w: Math.round(canvas.clientWidth * dpr),
205
+ h: Math.round(canvas.clientHeight * dpr),
206
+ dpr
207
+ };
208
+ };
209
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
210
+ const uniforms = gl ? initWebGL(gl) : null;
211
+ if (gl && uniforms) {
212
+ let contextLost = false;
213
+ const [cr, cg, cb] = parseHex(color);
214
+ const setUniforms = () => {
215
+ setUniform1f(gl, uniforms.uDensity, density);
216
+ setUniform1f(gl, uniforms.uOpacity, opacity);
217
+ setUniform1f(gl, uniforms.uFps, fps);
218
+ setUniform3f(gl, uniforms.uColor, cr / 255, cg / 255, cb / 255);
219
+ };
220
+ const onContextLost = (e) => {
221
+ e.preventDefault();
222
+ contextLost = true;
223
+ cancelAnimationFrame(rafRef.current);
224
+ };
225
+ const onContextRestored = () => {
226
+ contextLost = false;
227
+ const newUniforms = initWebGL(gl);
228
+ if (newUniforms) {
229
+ uniforms.uTime = newUniforms.uTime;
230
+ uniforms.uResolution = newUniforms.uResolution;
231
+ uniforms.uDensity = newUniforms.uDensity;
232
+ uniforms.uOpacity = newUniforms.uOpacity;
233
+ uniforms.uFps = newUniforms.uFps;
234
+ uniforms.uColor = newUniforms.uColor;
235
+ uniforms.program = newUniforms.program;
236
+ uniforms.vs = newUniforms.vs;
237
+ uniforms.fs = newUniforms.fs;
238
+ uniforms.buffer = newUniforms.buffer;
239
+ resize();
240
+ setUniforms();
241
+ if (!prefersReducedMotion)
242
+ rafRef.current = requestAnimationFrame(render);
243
+ }
244
+ };
245
+ canvas.addEventListener("webglcontextlost", onContextLost);
246
+ canvas.addEventListener("webglcontextrestored", onContextRestored);
247
+ const resize = () => {
248
+ const { w: w2, h: h2 } = getDimensions();
249
+ if (w2 === 0 || h2 === 0) return;
250
+ canvas.width = w2;
251
+ canvas.height = h2;
252
+ gl.viewport(0, 0, w2, h2);
253
+ setUniform2f(gl, uniforms.uResolution, w2, h2);
254
+ };
255
+ resize();
256
+ setUniforms();
257
+ gl.clearColor(0, 0, 0, 0);
258
+ const start = performance.now();
259
+ let lastSeed = -1;
260
+ const render = (now) => {
261
+ if (contextLost) return;
262
+ if (!document.hidden && visibleRef.current && canvas.width > 0 && canvas.height > 0) {
263
+ const time = (now - start) / 1e3;
264
+ const seed = Math.floor(time * fps);
265
+ if (seed !== lastSeed) {
266
+ lastSeed = seed;
267
+ gl.clear(gl.COLOR_BUFFER_BIT);
268
+ setUniform1f(gl, uniforms.uTime, time);
269
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
270
+ }
271
+ }
272
+ rafRef.current = requestAnimationFrame(render);
273
+ };
274
+ if (prefersReducedMotion) {
275
+ setUniform1f(gl, uniforms.uTime, 0);
276
+ gl.clear(gl.COLOR_BUFFER_BIT);
277
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
278
+ } else {
279
+ rafRef.current = requestAnimationFrame(render);
280
+ }
281
+ const onResize2 = () => {
282
+ if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
283
+ resizeTimerRef.current = setTimeout(resize, 150);
284
+ };
285
+ window.addEventListener("resize", onResize2);
286
+ return () => {
287
+ cancelAnimationFrame(rafRef.current);
288
+ if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
289
+ window.removeEventListener("resize", onResize2);
290
+ canvas.removeEventListener("webglcontextlost", onContextLost);
291
+ canvas.removeEventListener("webglcontextrestored", onContextRestored);
292
+ intersectionObs.disconnect();
293
+ gl.deleteBuffer(uniforms.buffer);
294
+ gl.deleteShader(uniforms.vs);
295
+ gl.deleteShader(uniforms.fs);
296
+ gl.deleteProgram(uniforms.program);
297
+ };
298
+ }
299
+ const ctx = canvas.getContext("2d");
300
+ if (!ctx) {
301
+ intersectionObs.disconnect();
302
+ return;
303
+ }
304
+ const padFactor = 0.3;
305
+ const getPad = (w2, h2) => Math.round(Math.max(w2, h2) * padFactor);
306
+ const setup = () => {
307
+ const { w: w2, h: h2 } = getDimensions();
308
+ if (w2 === 0 || h2 === 0) return { w: 0, h: 0, pad: 0 };
309
+ const pad2 = getPad(w2, h2);
310
+ canvas.width = w2;
311
+ canvas.height = h2;
312
+ if (!grainTextureRef.current || grainTextureRef.current.width < w2 + pad2 || grainTextureRef.current.height < h2 + pad2) {
313
+ grainTextureRef.current = generateGrainTexture(
314
+ w2 + pad2,
315
+ h2 + pad2,
316
+ density,
317
+ color
318
+ );
319
+ }
320
+ return { w: w2, h: h2, pad: pad2 };
321
+ };
322
+ let { w, h, pad } = setup();
323
+ let offsetX = 0;
324
+ let offsetY = 0;
325
+ const drawFrame = () => {
326
+ const grain = grainTextureRef.current;
327
+ if (!grain || w === 0 || h === 0) return;
328
+ const dpr = window.devicePixelRatio || 1;
329
+ ctx.clearRect(0, 0, w, h);
330
+ ctx.filter = `blur(${0.3 * dpr}px)`;
331
+ offsetX = (offsetX + 0.7) % pad;
332
+ offsetY = (offsetY + 0.5) % pad;
333
+ ctx.drawImage(grain, -offsetX, -offsetY, w + pad, h + pad);
334
+ ctx.filter = "none";
335
+ };
336
+ if (prefersReducedMotion) {
337
+ drawFrame();
338
+ intersectionObs.disconnect();
339
+ grainTextureRef.current = null;
340
+ return;
341
+ }
342
+ let lastFrame = performance.now();
343
+ const tick = (now) => {
344
+ if (!document.hidden && visibleRef.current && w > 0 && h > 0) {
345
+ while (now - lastFrame >= interval) {
346
+ lastFrame += interval;
347
+ drawFrame();
348
+ }
349
+ }
350
+ rafRef.current = requestAnimationFrame(tick);
351
+ };
352
+ rafRef.current = requestAnimationFrame(tick);
353
+ const onResize = () => {
354
+ if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
355
+ resizeTimerRef.current = setTimeout(() => {
356
+ const m = getDimensions();
357
+ const newPad = getPad(m.w, m.h);
358
+ if (grainTextureRef.current && grainTextureRef.current.width >= m.w + newPad && grainTextureRef.current.height >= m.h + newPad) {
359
+ w = m.w;
360
+ h = m.h;
361
+ pad = newPad;
362
+ canvas.width = w;
363
+ canvas.height = h;
364
+ return;
365
+ }
366
+ w = m.w;
367
+ h = m.h;
368
+ pad = newPad;
369
+ canvas.width = w;
370
+ canvas.height = h;
371
+ grainTextureRef.current = generateGrainTexture(
372
+ w + pad,
373
+ h + pad,
374
+ density,
375
+ color
376
+ );
377
+ }, 150);
378
+ };
379
+ window.addEventListener("resize", onResize);
380
+ return () => {
381
+ cancelAnimationFrame(rafRef.current);
382
+ if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
383
+ window.removeEventListener("resize", onResize);
384
+ intersectionObs.disconnect();
385
+ grainTextureRef.current = null;
386
+ };
387
+ }, [density, opacity, fps, interval, color]);
388
+ return canvasRef;
389
+ }
390
+ function FilmGrain({
391
+ density = 0.6,
392
+ opacity = 0.08,
393
+ blendMode = "overlay",
394
+ fps = 18,
395
+ color = "#ffffff",
396
+ className,
397
+ style
398
+ }) {
399
+ const canvasRef = useFilmGrain({ density, opacity, fps, color });
400
+ return /* @__PURE__ */ jsx(
401
+ "div",
402
+ {
403
+ "aria-hidden": "true",
404
+ className: [
405
+ "absolute inset-0 z-0 pointer-events-none select-none overflow-hidden",
406
+ className
407
+ ].filter(Boolean).join(" "),
408
+ style: { mixBlendMode: blendMode, opacity, ...style },
409
+ children: /* @__PURE__ */ jsx(
410
+ "canvas",
411
+ {
412
+ ref: canvasRef,
413
+ className: "w-full h-full will-change-transform"
414
+ }
415
+ )
416
+ }
417
+ );
418
+ }
419
+
420
+ export { FilmGrain };
421
+ //# sourceMappingURL=film-grain.js.map
422
+ //# sourceMappingURL=film-grain.js.map