@waveso/ui 0.7.4 → 0.7.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"animate.d.ts","names":[],"sources":["../src/animate.tsx"],"mappings":";;;;;;KAkBK,SAAA;AAAA,UAEK,kBAAA;EACR,QAAA,EAAU,YAAA;;EAEV,KAAA;EALY;EAOZ,IAAA,GAAO,SAAA;EAPK;EASZ,QAAA;EAPQ;EASR,KAAA;;EAEA,IAAA;EANO;EAQP,MAAA;EAQuB;EANvB,IAAA;EAdA;EAgBA,MAAA;EAdA;EAgBA,IAAA;EAdO;EAgBP,UAAA,GAAa,UAAA;AAAA;AAAA,UAGL,cAAA;EACR,QAAA,EAAU,YAAA;EAVV;EAYA,KAAA;EARA;EAUA,IAAA,GAAO,SAAA;EARM;EAUb,QAAA;EAVuB;EAYvB,KAAA;EATsB;EAWtB,IAAA;EAVU;EAYV,MAAA;EAMa;EAJb,IAAA;EAIuB;EAFvB,MAAA;EAhBU;EAkBV,UAAA,GAAa,UAAA;AAAA;AAAA,UAGL,YAAA;EACR,QAAA,EAAU,YAAA;EAdV;EAgBA,QAAA;EAZA;EAcA,SAAA;AAAA;AAAA,UAGQ,UAAA;EACR,QAAA,EAAU,YAAA;EAZa;EAcvB,GAAA;EAXQ;EAaR,GAAA;;EAEA,QAAA;EAdA;EAgBA,OAAA;EAdA;EAgBA,MAAA;AAAA;AAAA,UAGQ,UAAA;EACR,QAAA,EAAU,YAAA;EAfQ;EAiBlB,QAAA;EAhBsB;EAkBtB,QAAA;EAlBU;EAoBV,MAAA;EAhBA;EAkBA,MAAA;AAAA;;;;AAZM;;;;;;;;;;;;AAYA;iBA+HC,aAAA,CAAA;EACP,QAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EACA;AAAA,GACC,kBAAA,GAAkB,YAAA,mBAAA,OAAA,CAAA,qBAAA;;;;;;;;;;;;;;iBAoDZ,SAAA,CAAA;EACP,QAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA;EACA;AAAA,GACC,cAAA,GAAc,YAAA,mBAAA,OAAA,CAAA,qBAAA;;;;;;;;;;;;;iBA8CR,OAAA,CAAA;EACP,QAAA;EACA,QAAA;EACA;AAAA,GACC,YAAA,GAAY,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;;;;;iBA0BN,KAAA,CAAA;EACP,QAAA;EACA,GAAA;EACA,GAAA;EACA,QAAA;EACA,OAAA;EACA;AAAA,GACC,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;AAlJQ;;;;;;iBAsMZ,KAAA,CAAA;EACP,QAAA;EACA,QAAA;EACA,QAAA;EACA,MAAA;EACA;AAAA,GACC,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"animate.d.ts","names":[],"sources":["../src/animate.tsx"],"mappings":";;;;;;KAkBK,SAAA;AAAA,UAEK,kBAAA;EACR,QAAA,EAAU,YAAA;;EAEV,KAAA;EALY;EAOZ,IAAA,GAAO,SAAA;EAPK;EASZ,QAAA;EAPQ;EASR,KAAA;;EAEA,IAAA;EANO;EAQP,MAAA;EAQuB;EANvB,IAAA;EAdA;EAgBA,MAAA;EAdA;EAgBA,IAAA;EAdO;EAgBP,UAAA,GAAa,UAAA;AAAA;AAAA,UAGL,cAAA;EACR,QAAA,EAAU,YAAA;EAVV;EAYA,KAAA;EARA;EAUA,IAAA,GAAO,SAAA;EARM;EAUb,QAAA;EAVuB;EAYvB,KAAA;EATsB;EAWtB,IAAA;EAVU;EAYV,MAAA;EAMa;EAJb,IAAA;EAIuB;EAFvB,MAAA;EAhBU;EAkBV,UAAA,GAAa,UAAA;AAAA;AAAA,UAGL,YAAA;EACR,QAAA,EAAU,YAAA;EAdV;EAgBA,QAAA;EAZA;EAcA,SAAA;AAAA;AAAA,UAGQ,UAAA;EACR,QAAA,EAAU,YAAA;EAZa;EAcvB,GAAA;EAXQ;EAaR,GAAA;;EAEA,QAAA;EAdA;EAgBA,OAAA;EAdA;EAgBA,MAAA;AAAA;AAAA,UAGQ,UAAA;EACR,QAAA,EAAU,YAAA;EAfQ;EAiBlB,QAAA;EAhBsB;EAkBtB,QAAA;EAlBU;EAoBV,MAAA;EAhBA;EAkBA,MAAA;AAAA;;;;AAZM;;;;;;;;;;;;AAYA;iBA+HC,aAAA,CAAA;EACP,QAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EACA;AAAA,GACC,kBAAA,GAAkB,YAAA,mBAAA,OAAA,CAAA,qBAAA;;;;;;;;;;;;;;iBAkEZ,SAAA,CAAA;EACP,QAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA;EACA;AAAA,GACC,cAAA,GAAc,YAAA,mBAAA,OAAA,CAAA,qBAAA;;;;;;;;;;;;;iBA4DR,OAAA,CAAA;EACP,QAAA;EACA,QAAA;EACA;AAAA,GACC,YAAA,GAAY,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;;;;;iBA0BN,KAAA,CAAA;EACP,QAAA;EACA,GAAA;EACA,GAAA;EACA,QAAA;EACA,OAAA;EACA;AAAA,GACC,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;AA9KQ;;;;;;iBA4OZ,KAAA,CAAA;EACP,QAAA;EACA,QAAA;EACA,QAAA;EACA,MAAA;EACA;AAAA,GACC,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA"}
package/dist/animate.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Children, cloneElement, isValidElement, useEffect, useId, useRef, useState } from "react";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import { useInView } from "motion/react";
4
+ import { useInView, useReducedMotion } from "motion/react";
5
5
  //#region src/animate.tsx
6
6
  /**
7
7
  * Direction map: `from` means where the element COMES FROM.
@@ -112,6 +112,7 @@ function AnimateOnView({ children, delay = 0, from = "down", distance = 16, scal
112
112
  once,
113
113
  margin: "-50px"
114
114
  });
115
+ const prefersReducedMotion = useReducedMotion();
115
116
  useEffect(() => {
116
117
  setHydrated(true);
117
118
  }, []);
@@ -128,6 +129,13 @@ function AnimateOnView({ children, delay = 0, from = "down", distance = 16, scal
128
129
  rotate,
129
130
  flip
130
131
  });
132
+ if (prefersReducedMotion) return cloneElement(children, {
133
+ ref: mergeRefs(ref, existingRef),
134
+ style: {
135
+ ...existingStyle,
136
+ ...styles.visible
137
+ }
138
+ });
131
139
  const { duration, ease } = getTransitionParams(transition, spring, .4);
132
140
  const currentStyle = isInView ? styles.visible : styles.hidden;
133
141
  const transitionStr = buildTransitionStr(duration, ease, delay, styles.hasFilter);
@@ -157,6 +165,7 @@ function AnimateOnView({ children, delay = 0, from = "down", distance = 16, scal
157
165
  function AnimateIn({ children, delay = 0, from = "down", distance = 4, scale = false, blur = false, rotate = 0, flip = false, spring = false, transition }) {
158
166
  const ref = useRef(null);
159
167
  const [visible, setVisible] = useState(false);
168
+ const prefersReducedMotion = useReducedMotion();
160
169
  useEffect(() => {
161
170
  requestAnimationFrame(() => {
162
171
  requestAnimationFrame(() => setVisible(true));
@@ -174,6 +183,13 @@ function AnimateIn({ children, delay = 0, from = "down", distance = 4, scale = f
174
183
  rotate,
175
184
  flip
176
185
  });
186
+ if (prefersReducedMotion) return cloneElement(children, {
187
+ ref: mergeRefs(ref, existingRef),
188
+ style: {
189
+ ...existingStyle,
190
+ ...styles.visible
191
+ }
192
+ });
177
193
  const { duration, ease } = getTransitionParams(transition, spring);
178
194
  const currentStyle = visible ? styles.visible : styles.hidden;
179
195
  const transitionStr = buildTransitionStr(duration, ease, delay, styles.hasFilter);
@@ -219,20 +235,24 @@ function Stagger({ children, interval = .08, baseDelay = 0 }) {
219
235
  function Pulse({ children, min = .97, max = 1.03, duration = 2, opacity, paused = false }) {
220
236
  const id = useId().replace(/:/g, "");
221
237
  const [hydrated, setHydrated] = useState(false);
238
+ const prefersReducedMotion = useReducedMotion();
222
239
  useEffect(() => {
223
240
  setHydrated(true);
224
241
  }, []);
225
242
  if (!isValidElement(children)) return children;
226
243
  if (!hydrated) return children;
227
- const name = `pulse-${id}`;
228
- const keyframes = `@keyframes ${name} {
229
- 0%, 100% { transform: scale(${min}); opacity: ${opacity?.[0] ?? 1}; }
230
- 50% { transform: scale(${max}); opacity: ${opacity?.[1] ?? 1}; }
231
- }`;
232
244
  const childProps = children.props;
233
245
  const existingStyle = childProps.style ?? {};
234
246
  const existingRef = childProps.ref;
235
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", { children: keyframes }), cloneElement(children, {
247
+ if (prefersReducedMotion) return cloneElement(children, {
248
+ ref: existingRef ? mergeRefs(existingRef) : void 0,
249
+ style: existingStyle
250
+ });
251
+ const name = `pulse-${id}`;
252
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", { children: `@keyframes ${name} {
253
+ 0%, 100% { transform: scale(${min}); opacity: ${opacity?.[0] ?? 1}; }
254
+ 50% { transform: scale(${max}); opacity: ${opacity?.[1] ?? 1}; }
255
+ }` }), cloneElement(children, {
236
256
  ref: existingRef ? mergeRefs(existingRef) : void 0,
237
257
  style: {
238
258
  ...existingStyle,
@@ -255,22 +275,26 @@ function Pulse({ children, min = .97, max = 1.03, duration = 2, opacity, paused
255
275
  function Float({ children, distance = 6, duration = 3, rotate = 0, paused = false }) {
256
276
  const id = useId().replace(/:/g, "");
257
277
  const [hydrated, setHydrated] = useState(false);
278
+ const prefersReducedMotion = useReducedMotion();
258
279
  useEffect(() => {
259
280
  setHydrated(true);
260
281
  }, []);
261
282
  if (!isValidElement(children)) return children;
262
283
  if (!hydrated) return children;
284
+ const childProps = children.props;
285
+ const existingStyle = childProps.style ?? {};
286
+ const existingRef = childProps.ref;
287
+ if (prefersReducedMotion) return cloneElement(children, {
288
+ ...existingRef ? { ref: existingRef } : {},
289
+ style: existingStyle
290
+ });
263
291
  const name = `float-${id}`;
264
292
  const rotA = rotate ? ` rotate(${-rotate}deg)` : "";
265
293
  const rotB = rotate ? ` rotate(${rotate}deg)` : "";
266
- const keyframes = `@keyframes ${name} {
294
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", { children: `@keyframes ${name} {
267
295
  0%, 100% { transform: translateY(0px)${rotA}; }
268
296
  50% { transform: translateY(${-distance}px)${rotB}; }
269
- }`;
270
- const childProps = children.props;
271
- const existingStyle = childProps.style ?? {};
272
- const existingRef = childProps.ref;
273
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", { children: keyframes }), cloneElement(children, {
297
+ }` }), cloneElement(children, {
274
298
  ...existingRef ? { ref: existingRef } : {},
275
299
  style: {
276
300
  ...existingStyle,
@@ -1 +1 @@
1
- {"version":3,"file":"animate.js","names":[],"sources":["../src/animate.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n Children,\n type CSSProperties,\n type ReactElement,\n type Ref,\n cloneElement,\n isValidElement,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\"\nimport { useInView, type Transition } from \"motion/react\"\n\n// ── Types ────────────────────────────────────────────────────────────\n\ntype Direction = \"up\" | \"down\" | \"left\" | \"right\"\n\ninterface AnimateOnViewProps {\n children: ReactElement\n /** Delay in seconds before animation starts. Default: 0 */\n delay?: number\n /** Direction to animate from. Default: 'down' */\n from?: Direction\n /** Distance in px. Default: 16 (scroll reveals need more travel than `AnimateIn`) */\n distance?: number\n /** Also scale in (0.85 → 1). Default: false */\n scale?: boolean\n /** Blur in from a given amount in px. Default: false */\n blur?: boolean | number\n /** Rotate in from a given angle in degrees. Default: 0 */\n rotate?: number\n /** 3D flip entrance along the axis matching `from`. Default: false */\n flip?: boolean\n /** Spring easing with overshoot. Default: false */\n spring?: boolean\n /** Trigger once or every time it enters view. Default: true */\n once?: boolean\n /** Custom transition override */\n transition?: Transition\n}\n\ninterface AnimateInProps {\n children: ReactElement\n /** Delay in seconds before animation starts. Default: 0 */\n delay?: number\n /** Direction to animate from. Default: 'down' */\n from?: Direction\n /** Distance in px. Default: 4 */\n distance?: number\n /** Also scale in (0.85 → 1). Default: false */\n scale?: boolean\n /** Blur in from a given amount in px. Default: false */\n blur?: boolean | number\n /** Rotate in from a given angle in degrees. Default: 0 */\n rotate?: number\n /** 3D flip entrance along the axis matching `from`. Default: false */\n flip?: boolean\n /** Spring easing with overshoot. Default: false */\n spring?: boolean\n /** Custom transition override */\n transition?: Transition\n}\n\ninterface StaggerProps {\n children: ReactElement[]\n /** Delay increment between each child in seconds. Default: 0.08 */\n interval?: number\n /** Base delay before the first child animates in seconds. Default: 0 */\n baseDelay?: number\n}\n\ninterface PulseProps {\n children: ReactElement\n /** Min scale factor. Default: 0.97 */\n min?: number\n /** Max scale factor. Default: 1.03 */\n max?: number\n /** Animation duration in seconds. Default: 2 */\n duration?: number\n /** Also pulse opacity between these bounds [min, max]. Off by default. */\n opacity?: [number, number]\n /** Pause animation. Default: false */\n paused?: boolean\n}\n\ninterface FloatProps {\n children: ReactElement\n /** Vertical float distance in px. Default: 6 */\n distance?: number\n /** Animation duration in seconds. Default: 3 */\n duration?: number\n /** Also rotate slightly while floating (degrees). Default: 0 */\n rotate?: number\n /** Pause animation. Default: false */\n paused?: boolean\n}\n\n// ── Internals ────────────────────────────────────────────────────────\n\n/**\n * Direction map: `from` means where the element COMES FROM.\n * `from=\"left\"` = starts to the left, slides right into place.\n */\nconst DIRECTION_MAP = {\n up: { prop: \"translateY\", value: -1 },\n down: { prop: \"translateY\", value: 1 },\n left: { prop: \"translateX\", value: -1 },\n right: { prop: \"translateX\", value: 1 },\n} as const\n\n/** Flip axis: vertical directions flip around X, horizontal around Y */\nconst FLIP_MAP = {\n up: \"rotateX(90deg)\",\n down: \"rotateX(-90deg)\",\n left: \"rotateY(-90deg)\",\n right: \"rotateY(90deg)\",\n} as const\n\n/** Overshoot (\"spring\") easing for the opt-in `spring` prop on\n * `AnimateIn` / `AnimateOnView`. This is `animate.tsx`'s own page-entrance\n * bounce — the CSS recipe system intentionally has no spring, so this is a standalone constant, not a shared token. */\nconst SPRING_EASE = \"cubic-bezier(0.34, 1.45, 0.64, 1)\"\n\ninterface BuildStylesOptions {\n from: Direction\n distance: number\n doScale: boolean\n blur: boolean | number\n rotate: number\n flip: boolean\n}\n\nfunction buildStyles(opts: BuildStylesOptions) {\n const { from, distance, doScale, blur, rotate, flip } = opts\n const dir = DIRECTION_MAP[from]\n\n const hiddenParts: string[] = []\n if (flip) hiddenParts.push(\"perspective(800px)\")\n hiddenParts.push(`${dir.prop}(${dir.value * distance}px)`)\n if (doScale) hiddenParts.push(\"scale(0.85)\")\n if (rotate) hiddenParts.push(`rotate(${rotate}deg)`)\n if (flip) hiddenParts.push(FLIP_MAP[from])\n\n const visibleParts: string[] = []\n if (flip) visibleParts.push(\"perspective(800px)\")\n visibleParts.push(\"translateX(0) translateY(0)\")\n if (doScale) visibleParts.push(\"scale(1)\")\n if (rotate) visibleParts.push(\"rotate(0deg)\")\n if (flip) visibleParts.push(from === \"up\" || from === \"down\" ? \"rotateX(0deg)\" : \"rotateY(0deg)\")\n\n const blurPx = blur === true ? 8 : typeof blur === \"number\" ? blur : 0\n\n const hidden: CSSProperties = {\n opacity: \"0\",\n transform: hiddenParts.join(\" \"),\n }\n\n const visible: CSSProperties = {\n opacity: \"1\",\n transform: visibleParts.join(\" \"),\n }\n\n if (blurPx > 0) {\n hidden.filter = `blur(${blurPx}px)`\n visible.filter = \"blur(0px)\"\n }\n\n return { hidden, visible, hasFilter: blurPx > 0 }\n}\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]) {\n return (el: T | null) => {\n refs.forEach((ref) => {\n if (typeof ref === \"function\") ref(el)\n else if (ref && typeof ref === \"object\") {\n ;(ref as { current: T | null }).current = el\n }\n })\n }\n}\n\nfunction getTransitionParams(transition?: Transition, useSpring?: boolean, fallbackDuration = 0.15) {\n const duration =\n (transition as Record<string, number>)?.duration ?? fallbackDuration\n const ease =\n useSpring ? SPRING_EASE : ((transition as Record<string, string>)?.ease ?? \"ease-out\")\n return { duration, ease }\n}\n\nfunction buildTransitionStr(\n duration: number,\n ease: string,\n delay: number,\n hasFilter: boolean,\n) {\n const parts = [\n `opacity ${duration}s ${ease} ${delay}s`,\n `transform ${duration}s ${ease} ${delay}s`,\n ]\n if (hasFilter) parts.push(`filter ${duration}s ${ease} ${delay}s`)\n return parts.join(\", \")\n}\n\n// ── AnimateOnView ────────────────────────────────────────────────────\n\n/**\n * Animates a child element when it scrolls into view.\n *\n * Defaults are tuned for scroll reveals — larger travel and a slower duration\n * than `AnimateIn` — so the motion reads as the content enters the viewport.\n *\n * Zero DOM overhead — applies styles directly to the child via cloneElement.\n * Renders children unchanged on server to avoid hydration mismatch.\n *\n * @example\n * ```tsx\n * <AnimateOnView from=\"up\" blur scale>\n * <Card>Hello</Card>\n * </AnimateOnView>\n * ```\n */\nfunction AnimateOnView({\n children,\n delay = 0,\n from = \"down\",\n distance = 16,\n scale = false,\n blur = false,\n rotate = 0,\n flip = false,\n spring = false,\n once = true,\n transition,\n}: AnimateOnViewProps) {\n const ref = useRef<HTMLElement>(null)\n const [hydrated, setHydrated] = useState(false)\n const isInView = useInView(ref, { once, margin: \"-50px\" })\n\n useEffect(() => {\n setHydrated(true)\n }, [])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n if (!hydrated) {\n return cloneElement(children, {\n ref: mergeRefs(ref, existingRef),\n } as Record<string, unknown>)\n }\n\n const styles = buildStyles({ from, distance, doScale: scale, blur, rotate, flip })\n const { duration, ease } = getTransitionParams(transition, spring, 0.4)\n const currentStyle = isInView ? styles.visible : styles.hidden\n const transitionStr = buildTransitionStr(duration, ease, delay, styles.hasFilter)\n\n return cloneElement(children, {\n ref: mergeRefs(ref, existingRef),\n style: {\n ...existingStyle,\n ...currentStyle,\n ...(isInView ? { transition: transitionStr } : {}),\n willChange: \"opacity, transform\",\n },\n } as Record<string, unknown>)\n}\n\n// ── AnimateIn ────────────────────────────────────────────────────────\n\n/**\n * Animates a child element immediately on mount.\n *\n * Zero DOM overhead — applies styles directly to the child via cloneElement.\n * Uses a two-phase approach: hidden → visible with requestAnimationFrame.\n *\n * @example\n * ```tsx\n * <AnimateIn from=\"up\" blur spring>\n * <h1>Welcome</h1>\n * </AnimateIn>\n * ```\n */\nfunction AnimateIn({\n children,\n delay = 0,\n from = \"down\",\n distance = 4,\n scale = false,\n blur = false,\n rotate = 0,\n flip = false,\n spring = false,\n transition,\n}: AnimateInProps) {\n const ref = useRef<HTMLElement>(null)\n const [visible, setVisible] = useState(false)\n\n useEffect(() => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => setVisible(true))\n })\n }, [])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n const styles = buildStyles({ from, distance, doScale: scale, blur, rotate, flip })\n const { duration, ease } = getTransitionParams(transition, spring)\n const currentStyle = visible ? styles.visible : styles.hidden\n const transitionStr = buildTransitionStr(duration, ease, delay, styles.hasFilter)\n\n return cloneElement(children, {\n ref: mergeRefs(ref, existingRef),\n style: {\n ...existingStyle,\n ...currentStyle,\n ...(visible ? { transition: transitionStr } : {}),\n willChange: \"opacity, transform\",\n },\n } as Record<string, unknown>)\n}\n\n// ── Stagger ──────────────────────────────────────────────────────────\n\n/**\n * Auto-staggers delay on child AnimateIn/AnimateOnView elements.\n * Eliminates manual `delay={i * 0.08}` math.\n *\n * @example\n * ```tsx\n * <Stagger interval={0.1}>\n * <AnimateIn from=\"up\"><Card>One</Card></AnimateIn>\n * <AnimateIn from=\"up\"><Card>Two</Card></AnimateIn>\n * </Stagger>\n * ```\n */\nfunction Stagger({\n children,\n interval = 0.08,\n baseDelay = 0,\n}: StaggerProps) {\n return (\n <>\n {Children.map(children, (child, index) => {\n if (!isValidElement(child)) return child\n return cloneElement(child as ReactElement<{ delay?: number }>, {\n delay: baseDelay + index * interval,\n })\n })}\n </>\n )\n}\n\n// ── Pulse ────────────────────────────────────────────────────────────\n\n/**\n * Continuous, subtle scale pulse. Great for live indicators and CTAs.\n * Zero DOM overhead — injects a scoped keyframe via `<style>`.\n *\n * @example\n * ```tsx\n * <Pulse>\n * <span className=\"size-3 rounded-full bg-green-500\" />\n * </Pulse>\n * ```\n */\nfunction Pulse({\n children,\n min = 0.97,\n max = 1.03,\n duration = 2,\n opacity,\n paused = false,\n}: PulseProps) {\n const id = useId().replace(/:/g, \"\")\n const [hydrated, setHydrated] = useState(false)\n\n useEffect(() => {\n setHydrated(true)\n }, [])\n\n if (!isValidElement(children)) return children\n if (!hydrated) return children\n\n const name = `pulse-${id}`\n const opacityFrom = opacity?.[0] ?? 1\n const opacityTo = opacity?.[1] ?? 1\n\n const keyframes = `@keyframes ${name} {\n 0%, 100% { transform: scale(${min}); opacity: ${opacityFrom}; }\n 50% { transform: scale(${max}); opacity: ${opacityTo}; }\n}`\n\n const childProps = children.props as Record<string, unknown>\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n return (\n <>\n <style>{keyframes}</style>\n {cloneElement(children, {\n ref: existingRef ? mergeRefs(existingRef) : undefined,\n style: {\n ...existingStyle,\n animation: `${name} ${duration}s ease-in-out infinite`,\n animationPlayState: paused ? \"paused\" : \"running\",\n },\n } as Record<string, unknown>)}\n </>\n )\n}\n\n// ── Float ────────────────────────────────────────────────────────────\n\n/**\n * Gentle continuous up/down float. Perfect for decorative elements.\n * Zero DOM overhead — injects a scoped keyframe via `<style>`.\n *\n * @example\n * ```tsx\n * <Float distance={10} duration={4}>\n * <Card>Floating</Card>\n * </Float>\n * ```\n */\nfunction Float({\n children,\n distance = 6,\n duration = 3,\n rotate = 0,\n paused = false,\n}: FloatProps) {\n const id = useId().replace(/:/g, \"\")\n const [hydrated, setHydrated] = useState(false)\n\n useEffect(() => {\n setHydrated(true)\n }, [])\n\n if (!isValidElement(children)) return children\n if (!hydrated) return children\n\n const name = `float-${id}`\n const rotA = rotate ? ` rotate(${-rotate}deg)` : \"\"\n const rotB = rotate ? ` rotate(${rotate}deg)` : \"\"\n\n const keyframes = `@keyframes ${name} {\n 0%, 100% { transform: translateY(0px)${rotA}; }\n 50% { transform: translateY(${-distance}px)${rotB}; }\n}`\n\n const childProps = children.props as Record<string, unknown>\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n return (\n <>\n <style>{keyframes}</style>\n {cloneElement(children, {\n ...(existingRef ? { ref: existingRef } : {}),\n style: {\n ...existingStyle,\n animation: `${name} ${duration}s ease-in-out infinite`,\n animationPlayState: paused ? \"paused\" : \"running\",\n },\n } as Record<string, unknown>)}\n </>\n )\n}\n\n// ── Exports ──────────────────────────────────────────────────────────\n\nexport { AnimateOnView, AnimateIn, Stagger, Pulse, Float }\nexport type {\n AnimateOnViewProps,\n AnimateInProps,\n StaggerProps,\n PulseProps,\n FloatProps,\n Direction,\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"animate.js","names":[],"sources":["../src/animate.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n Children,\n type CSSProperties,\n type ReactElement,\n type Ref,\n cloneElement,\n isValidElement,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\"\nimport { useInView, useReducedMotion, type Transition } from \"motion/react\"\n\n// ── Types ────────────────────────────────────────────────────────────\n\ntype Direction = \"up\" | \"down\" | \"left\" | \"right\"\n\ninterface AnimateOnViewProps {\n children: ReactElement\n /** Delay in seconds before animation starts. Default: 0 */\n delay?: number\n /** Direction to animate from. Default: 'down' */\n from?: Direction\n /** Distance in px. Default: 16 (scroll reveals need more travel than `AnimateIn`) */\n distance?: number\n /** Also scale in (0.85 → 1). Default: false */\n scale?: boolean\n /** Blur in from a given amount in px. Default: false */\n blur?: boolean | number\n /** Rotate in from a given angle in degrees. Default: 0 */\n rotate?: number\n /** 3D flip entrance along the axis matching `from`. Default: false */\n flip?: boolean\n /** Spring easing with overshoot. Default: false */\n spring?: boolean\n /** Trigger once or every time it enters view. Default: true */\n once?: boolean\n /** Custom transition override */\n transition?: Transition\n}\n\ninterface AnimateInProps {\n children: ReactElement\n /** Delay in seconds before animation starts. Default: 0 */\n delay?: number\n /** Direction to animate from. Default: 'down' */\n from?: Direction\n /** Distance in px. Default: 4 */\n distance?: number\n /** Also scale in (0.85 → 1). Default: false */\n scale?: boolean\n /** Blur in from a given amount in px. Default: false */\n blur?: boolean | number\n /** Rotate in from a given angle in degrees. Default: 0 */\n rotate?: number\n /** 3D flip entrance along the axis matching `from`. Default: false */\n flip?: boolean\n /** Spring easing with overshoot. Default: false */\n spring?: boolean\n /** Custom transition override */\n transition?: Transition\n}\n\ninterface StaggerProps {\n children: ReactElement[]\n /** Delay increment between each child in seconds. Default: 0.08 */\n interval?: number\n /** Base delay before the first child animates in seconds. Default: 0 */\n baseDelay?: number\n}\n\ninterface PulseProps {\n children: ReactElement\n /** Min scale factor. Default: 0.97 */\n min?: number\n /** Max scale factor. Default: 1.03 */\n max?: number\n /** Animation duration in seconds. Default: 2 */\n duration?: number\n /** Also pulse opacity between these bounds [min, max]. Off by default. */\n opacity?: [number, number]\n /** Pause animation. Default: false */\n paused?: boolean\n}\n\ninterface FloatProps {\n children: ReactElement\n /** Vertical float distance in px. Default: 6 */\n distance?: number\n /** Animation duration in seconds. Default: 3 */\n duration?: number\n /** Also rotate slightly while floating (degrees). Default: 0 */\n rotate?: number\n /** Pause animation. Default: false */\n paused?: boolean\n}\n\n// ── Internals ────────────────────────────────────────────────────────\n\n/**\n * Direction map: `from` means where the element COMES FROM.\n * `from=\"left\"` = starts to the left, slides right into place.\n */\nconst DIRECTION_MAP = {\n up: { prop: \"translateY\", value: -1 },\n down: { prop: \"translateY\", value: 1 },\n left: { prop: \"translateX\", value: -1 },\n right: { prop: \"translateX\", value: 1 },\n} as const\n\n/** Flip axis: vertical directions flip around X, horizontal around Y */\nconst FLIP_MAP = {\n up: \"rotateX(90deg)\",\n down: \"rotateX(-90deg)\",\n left: \"rotateY(-90deg)\",\n right: \"rotateY(90deg)\",\n} as const\n\n/** Overshoot (\"spring\") easing for the opt-in `spring` prop on\n * `AnimateIn` / `AnimateOnView`. This is `animate.tsx`'s own page-entrance\n * bounce — the CSS recipe system intentionally has no spring, so this is a standalone constant, not a shared token. */\nconst SPRING_EASE = \"cubic-bezier(0.34, 1.45, 0.64, 1)\"\n\ninterface BuildStylesOptions {\n from: Direction\n distance: number\n doScale: boolean\n blur: boolean | number\n rotate: number\n flip: boolean\n}\n\nfunction buildStyles(opts: BuildStylesOptions) {\n const { from, distance, doScale, blur, rotate, flip } = opts\n const dir = DIRECTION_MAP[from]\n\n const hiddenParts: string[] = []\n if (flip) hiddenParts.push(\"perspective(800px)\")\n hiddenParts.push(`${dir.prop}(${dir.value * distance}px)`)\n if (doScale) hiddenParts.push(\"scale(0.85)\")\n if (rotate) hiddenParts.push(`rotate(${rotate}deg)`)\n if (flip) hiddenParts.push(FLIP_MAP[from])\n\n const visibleParts: string[] = []\n if (flip) visibleParts.push(\"perspective(800px)\")\n visibleParts.push(\"translateX(0) translateY(0)\")\n if (doScale) visibleParts.push(\"scale(1)\")\n if (rotate) visibleParts.push(\"rotate(0deg)\")\n if (flip) visibleParts.push(from === \"up\" || from === \"down\" ? \"rotateX(0deg)\" : \"rotateY(0deg)\")\n\n const blurPx = blur === true ? 8 : typeof blur === \"number\" ? blur : 0\n\n const hidden: CSSProperties = {\n opacity: \"0\",\n transform: hiddenParts.join(\" \"),\n }\n\n const visible: CSSProperties = {\n opacity: \"1\",\n transform: visibleParts.join(\" \"),\n }\n\n if (blurPx > 0) {\n hidden.filter = `blur(${blurPx}px)`\n visible.filter = \"blur(0px)\"\n }\n\n return { hidden, visible, hasFilter: blurPx > 0 }\n}\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]) {\n return (el: T | null) => {\n refs.forEach((ref) => {\n if (typeof ref === \"function\") ref(el)\n else if (ref && typeof ref === \"object\") {\n ;(ref as { current: T | null }).current = el\n }\n })\n }\n}\n\nfunction getTransitionParams(transition?: Transition, useSpring?: boolean, fallbackDuration = 0.15) {\n const duration =\n (transition as Record<string, number>)?.duration ?? fallbackDuration\n const ease =\n useSpring ? SPRING_EASE : ((transition as Record<string, string>)?.ease ?? \"ease-out\")\n return { duration, ease }\n}\n\nfunction buildTransitionStr(\n duration: number,\n ease: string,\n delay: number,\n hasFilter: boolean,\n) {\n const parts = [\n `opacity ${duration}s ${ease} ${delay}s`,\n `transform ${duration}s ${ease} ${delay}s`,\n ]\n if (hasFilter) parts.push(`filter ${duration}s ${ease} ${delay}s`)\n return parts.join(\", \")\n}\n\n// ── AnimateOnView ────────────────────────────────────────────────────\n\n/**\n * Animates a child element when it scrolls into view.\n *\n * Defaults are tuned for scroll reveals — larger travel and a slower duration\n * than `AnimateIn` — so the motion reads as the content enters the viewport.\n *\n * Zero DOM overhead — applies styles directly to the child via cloneElement.\n * Renders children unchanged on server to avoid hydration mismatch.\n *\n * @example\n * ```tsx\n * <AnimateOnView from=\"up\" blur scale>\n * <Card>Hello</Card>\n * </AnimateOnView>\n * ```\n */\nfunction AnimateOnView({\n children,\n delay = 0,\n from = \"down\",\n distance = 16,\n scale = false,\n blur = false,\n rotate = 0,\n flip = false,\n spring = false,\n once = true,\n transition,\n}: AnimateOnViewProps) {\n const ref = useRef<HTMLElement>(null)\n const [hydrated, setHydrated] = useState(false)\n const isInView = useInView(ref, { once, margin: \"-50px\" })\n const prefersReducedMotion = useReducedMotion()\n\n useEffect(() => {\n setHydrated(true)\n }, [])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n if (!hydrated) {\n return cloneElement(children, {\n ref: mergeRefs(ref, existingRef),\n } as Record<string, unknown>)\n }\n\n const styles = buildStyles({ from, distance, doScale: scale, blur, rotate, flip })\n\n // Reduced motion: render the final, resting state immediately — no\n // travel, blur, scale, or transition. Keeps the same DOM/output shape.\n if (prefersReducedMotion) {\n return cloneElement(children, {\n ref: mergeRefs(ref, existingRef),\n style: {\n ...existingStyle,\n ...styles.visible,\n },\n } as Record<string, unknown>)\n }\n\n const { duration, ease } = getTransitionParams(transition, spring, 0.4)\n const currentStyle = isInView ? styles.visible : styles.hidden\n const transitionStr = buildTransitionStr(duration, ease, delay, styles.hasFilter)\n\n return cloneElement(children, {\n ref: mergeRefs(ref, existingRef),\n style: {\n ...existingStyle,\n ...currentStyle,\n ...(isInView ? { transition: transitionStr } : {}),\n willChange: \"opacity, transform\",\n },\n } as Record<string, unknown>)\n}\n\n// ── AnimateIn ────────────────────────────────────────────────────────\n\n/**\n * Animates a child element immediately on mount.\n *\n * Zero DOM overhead — applies styles directly to the child via cloneElement.\n * Uses a two-phase approach: hidden → visible with requestAnimationFrame.\n *\n * @example\n * ```tsx\n * <AnimateIn from=\"up\" blur spring>\n * <h1>Welcome</h1>\n * </AnimateIn>\n * ```\n */\nfunction AnimateIn({\n children,\n delay = 0,\n from = \"down\",\n distance = 4,\n scale = false,\n blur = false,\n rotate = 0,\n flip = false,\n spring = false,\n transition,\n}: AnimateInProps) {\n const ref = useRef<HTMLElement>(null)\n const [visible, setVisible] = useState(false)\n const prefersReducedMotion = useReducedMotion()\n\n useEffect(() => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => setVisible(true))\n })\n }, [])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n const styles = buildStyles({ from, distance, doScale: scale, blur, rotate, flip })\n\n // Reduced motion: render the final, resting state immediately — no\n // travel, blur, scale, or transition. Keeps the same DOM/output shape.\n if (prefersReducedMotion) {\n return cloneElement(children, {\n ref: mergeRefs(ref, existingRef),\n style: {\n ...existingStyle,\n ...styles.visible,\n },\n } as Record<string, unknown>)\n }\n\n const { duration, ease } = getTransitionParams(transition, spring)\n const currentStyle = visible ? styles.visible : styles.hidden\n const transitionStr = buildTransitionStr(duration, ease, delay, styles.hasFilter)\n\n return cloneElement(children, {\n ref: mergeRefs(ref, existingRef),\n style: {\n ...existingStyle,\n ...currentStyle,\n ...(visible ? { transition: transitionStr } : {}),\n willChange: \"opacity, transform\",\n },\n } as Record<string, unknown>)\n}\n\n// ── Stagger ──────────────────────────────────────────────────────────\n\n/**\n * Auto-staggers delay on child AnimateIn/AnimateOnView elements.\n * Eliminates manual `delay={i * 0.08}` math.\n *\n * @example\n * ```tsx\n * <Stagger interval={0.1}>\n * <AnimateIn from=\"up\"><Card>One</Card></AnimateIn>\n * <AnimateIn from=\"up\"><Card>Two</Card></AnimateIn>\n * </Stagger>\n * ```\n */\nfunction Stagger({\n children,\n interval = 0.08,\n baseDelay = 0,\n}: StaggerProps) {\n return (\n <>\n {Children.map(children, (child, index) => {\n if (!isValidElement(child)) return child\n return cloneElement(child as ReactElement<{ delay?: number }>, {\n delay: baseDelay + index * interval,\n })\n })}\n </>\n )\n}\n\n// ── Pulse ────────────────────────────────────────────────────────────\n\n/**\n * Continuous, subtle scale pulse. Great for live indicators and CTAs.\n * Zero DOM overhead — injects a scoped keyframe via `<style>`.\n *\n * @example\n * ```tsx\n * <Pulse>\n * <span className=\"size-3 rounded-full bg-green-500\" />\n * </Pulse>\n * ```\n */\nfunction Pulse({\n children,\n min = 0.97,\n max = 1.03,\n duration = 2,\n opacity,\n paused = false,\n}: PulseProps) {\n const id = useId().replace(/:/g, \"\")\n const [hydrated, setHydrated] = useState(false)\n const prefersReducedMotion = useReducedMotion()\n\n useEffect(() => {\n setHydrated(true)\n }, [])\n\n if (!isValidElement(children)) return children\n if (!hydrated) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n // Reduced motion: no continuous pulse — render the child at rest, no\n // keyframe injected. Same ref/style handling as the animated path.\n if (prefersReducedMotion) {\n return cloneElement(children, {\n ref: existingRef ? mergeRefs(existingRef) : undefined,\n style: existingStyle,\n } as Record<string, unknown>)\n }\n\n const name = `pulse-${id}`\n const opacityFrom = opacity?.[0] ?? 1\n const opacityTo = opacity?.[1] ?? 1\n\n const keyframes = `@keyframes ${name} {\n 0%, 100% { transform: scale(${min}); opacity: ${opacityFrom}; }\n 50% { transform: scale(${max}); opacity: ${opacityTo}; }\n}`\n\n return (\n <>\n <style>{keyframes}</style>\n {cloneElement(children, {\n ref: existingRef ? mergeRefs(existingRef) : undefined,\n style: {\n ...existingStyle,\n animation: `${name} ${duration}s ease-in-out infinite`,\n animationPlayState: paused ? \"paused\" : \"running\",\n },\n } as Record<string, unknown>)}\n </>\n )\n}\n\n// ── Float ────────────────────────────────────────────────────────────\n\n/**\n * Gentle continuous up/down float. Perfect for decorative elements.\n * Zero DOM overhead — injects a scoped keyframe via `<style>`.\n *\n * @example\n * ```tsx\n * <Float distance={10} duration={4}>\n * <Card>Floating</Card>\n * </Float>\n * ```\n */\nfunction Float({\n children,\n distance = 6,\n duration = 3,\n rotate = 0,\n paused = false,\n}: FloatProps) {\n const id = useId().replace(/:/g, \"\")\n const [hydrated, setHydrated] = useState(false)\n const prefersReducedMotion = useReducedMotion()\n\n useEffect(() => {\n setHydrated(true)\n }, [])\n\n if (!isValidElement(children)) return children\n if (!hydrated) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n // Reduced motion: no continuous float — render the child at rest, no\n // keyframe injected. Same ref/style handling as the animated path.\n if (prefersReducedMotion) {\n return cloneElement(children, {\n ...(existingRef ? { ref: existingRef } : {}),\n style: existingStyle,\n } as Record<string, unknown>)\n }\n\n const name = `float-${id}`\n const rotA = rotate ? ` rotate(${-rotate}deg)` : \"\"\n const rotB = rotate ? ` rotate(${rotate}deg)` : \"\"\n\n const keyframes = `@keyframes ${name} {\n 0%, 100% { transform: translateY(0px)${rotA}; }\n 50% { transform: translateY(${-distance}px)${rotB}; }\n}`\n\n return (\n <>\n <style>{keyframes}</style>\n {cloneElement(children, {\n ...(existingRef ? { ref: existingRef } : {}),\n style: {\n ...existingStyle,\n animation: `${name} ${duration}s ease-in-out infinite`,\n animationPlayState: paused ? \"paused\" : \"running\",\n },\n } as Record<string, unknown>)}\n </>\n )\n}\n\n// ── Exports ──────────────────────────────────────────────────────────\n\nexport { AnimateOnView, AnimateIn, Stagger, Pulse, Float }\nexport type {\n AnimateOnViewProps,\n AnimateInProps,\n StaggerProps,\n PulseProps,\n FloatProps,\n Direction,\n}\n"],"mappings":"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"}
package/dist/badge.d.ts CHANGED
@@ -5,7 +5,7 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
5
5
 
6
6
  //#region src/badge.d.ts
7
7
  declare const badgeVariants: (props?: ({
8
- variant?: "default" | "link" | "success" | "destructive" | "warning" | "secondary" | "outline" | "ghost" | null | undefined;
8
+ variant?: "default" | "outline" | "secondary" | "ghost" | "success" | "destructive" | "link" | "warning" | null | undefined;
9
9
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
10
10
  type BadgeProps = useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>;
11
11
  declare function Badge({
package/dist/button.d.ts CHANGED
@@ -6,8 +6,8 @@ import * as _$class_variance_authority_types0 from "class-variance-authority/typ
6
6
 
7
7
  //#region src/button.d.ts
8
8
  declare const buttonVariants: (props?: ({
9
- variant?: "default" | "link" | "success" | "destructive" | "secondary" | "outline" | "ghost" | null | undefined;
10
- size?: "default" | "icon" | "xs" | "sm" | "lg" | "xl" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
9
+ variant?: "default" | "outline" | "secondary" | "ghost" | "success" | "destructive" | "link" | null | undefined;
10
+ size?: "default" | "xs" | "sm" | "icon-xs" | "icon-sm" | "lg" | "xl" | "icon" | "icon-lg" | null | undefined;
11
11
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
12
12
  type ButtonProps = React.ComponentProps<typeof Button$1> & VariantProps<typeof buttonVariants>;
13
13
  declare function Button({
@@ -1 +1 @@
1
- {"version":3,"file":"count.d.ts","names":[],"sources":["../src/count.tsx"],"mappings":";;;;UAeU,UAAA;;EAER,EAAA,WAAa,IAAA;EAFL;EAIR,IAAA;;EAEA,QAAA;EAJA;EAMA,KAAA;EAJA;;;;;;EAWA,MAAA,IAAU,KAAA;EAMV;EAJA,MAAA;EAMA;EAJA,MAAA;EAMU;EAJV,QAAA,EAAU,YAAA;EAMA;EAJV,IAAA;EAQG;EANH,MAAA,IAAU,CAAA;;EAEV,UAAA;AAAA;AAI4B;AAAA,KAAzB,YAAA,GAAe,UAAA;;iBAKX,OAAA,CAAQ,CAAA;;AAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwEjB,KAAA,CAAA;EACP,EAAA;EACA,IAAA,EAAM,KAAA;EACN,QAAA;EACA,KAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;EACA;AAAA,GACC,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;;cAkMP,OAAA,SAAO,KAAA"}
1
+ {"version":3,"file":"count.d.ts","names":[],"sources":["../src/count.tsx"],"mappings":";;;;UAeU,UAAA;;EAER,EAAA,WAAa,IAAA;EAFL;EAIR,IAAA;;EAEA,QAAA;EAJA;EAMA,KAAA;EAJA;;;;;;EAWA,MAAA,IAAU,KAAA;EAMV;EAJA,MAAA;EAMA;EAJA,MAAA;EAMU;EAJV,QAAA,EAAU,YAAA;EAMA;EAJV,IAAA;EAQG;EANH,MAAA,IAAU,CAAA;;EAEV,UAAA;AAAA;AAI4B;AAAA,KAAzB,YAAA,GAAe,UAAA;;iBAKX,OAAA,CAAQ,CAAA;;AAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwEjB,KAAA,CAAA;EACP,EAAA;EACA,IAAA,EAAM,KAAA;EACN,QAAA;EACA,KAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;EACA;AAAA,GACC,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;;cAiNP,OAAA,SAAO,KAAA"}
package/dist/count.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cloneElement, isValidElement, useEffect, useRef, useState } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
- import { useInView } from "motion/react";
4
+ import { useInView, useReducedMotion } from "motion/react";
5
5
  //#region src/count.tsx
6
6
  /** Cubic ease-out: fast start, smooth deceleration */
7
7
  function easeOut(t) {
@@ -88,6 +88,7 @@ function NumberCount({ to, from: start, duration, delay, format, prefix, suffix,
88
88
  once,
89
89
  margin: "-50px"
90
90
  });
91
+ const prefersReducedMotion = useReducedMotion();
91
92
  const [display, setDisplay] = useState(start);
92
93
  const easingRef = useRef(easing);
93
94
  const onCompleteRef = useRef(onComplete);
@@ -99,6 +100,13 @@ function NumberCount({ to, from: start, duration, delay, format, prefix, suffix,
99
100
  useEffect(() => {
100
101
  if (!isInView) return;
101
102
  const delayMs = delay * 1e3;
103
+ if (prefersReducedMotion) {
104
+ const timer = setTimeout(() => {
105
+ setDisplay(to);
106
+ onCompleteRef.current?.();
107
+ }, delayMs);
108
+ return () => clearTimeout(timer);
109
+ }
102
110
  let raf;
103
111
  let startTime;
104
112
  const timer = setTimeout(() => {
@@ -121,7 +129,8 @@ function NumberCount({ to, from: start, duration, delay, format, prefix, suffix,
121
129
  to,
122
130
  start,
123
131
  duration,
124
- delay
132
+ delay,
133
+ prefersReducedMotion
125
134
  ]);
126
135
  if (!isValidElement(children)) return children;
127
136
  const existingRef = children.props.ref;
package/dist/count.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"count.js","names":[],"sources":["../src/count.tsx"],"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\n // Keep the callbacks in refs so a parent re-render with inline `easing` /\n // `onComplete` props doesn't re-run the animation effect — which would cancel\n // the in-flight rAF and freeze the counter mid-count. The effect below keys\n // only on the values that should actually (re)start the animation, so a live\n // `to` change or a re-entry (with `once={false}`) restarts cleanly.\n const easingRef = useRef(easing)\n const onCompleteRef = useRef(onComplete)\n useEffect(() => {\n easingRef.current = easing\n onCompleteRef.current = onComplete\n })\n\n const formatFn = format ?? ((n: number) =>\n Number.isInteger(to) ? Math.round(n).toLocaleString() : n.toLocaleString()\n )\n\n useEffect(() => {\n if (!isInView) return\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 current = start + (to - start) * easingRef.current(progress)\n\n setDisplay(current)\n\n if (progress < 1) {\n raf = requestAnimationFrame(animate)\n } else {\n onCompleteRef.current?.()\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])\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"],"mappings":"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"}
1
+ {"version":3,"file":"count.js","names":[],"sources":["../src/count.tsx"],"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, useReducedMotion } 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 prefersReducedMotion = useReducedMotion()\n const [display, setDisplay] = useState(start)\n\n // Keep the callbacks in refs so a parent re-render with inline `easing` /\n // `onComplete` props doesn't re-run the animation effect — which would cancel\n // the in-flight rAF and freeze the counter mid-count. The effect below keys\n // only on the values that should actually (re)start the animation, so a live\n // `to` change or a re-entry (with `once={false}`) restarts cleanly.\n const easingRef = useRef(easing)\n const onCompleteRef = useRef(onComplete)\n useEffect(() => {\n easingRef.current = easing\n onCompleteRef.current = onComplete\n })\n\n const formatFn = format ?? ((n: number) =>\n Number.isInteger(to) ? Math.round(n).toLocaleString() : n.toLocaleString()\n )\n\n useEffect(() => {\n if (!isInView) return\n\n const delayMs = delay * 1000\n\n // Vestibular safety: skip the count-up/down tween entirely under\n // prefers-reduced-motion and snap to the final value. We still honor the\n // in-view gate and `delay`, then fire `onComplete` once — matching the\n // resting state and lifecycle of the animated path.\n if (prefersReducedMotion) {\n const timer = setTimeout(() => {\n setDisplay(to)\n onCompleteRef.current?.()\n }, delayMs)\n\n return () => clearTimeout(timer)\n }\n\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 current = start + (to - start) * easingRef.current(progress)\n\n setDisplay(current)\n\n if (progress < 1) {\n raf = requestAnimationFrame(animate)\n } else {\n onCompleteRef.current?.()\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, prefersReducedMotion])\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"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"encrypted-text.d.ts","names":[],"sources":["../src/encrypted-text.tsx"],"mappings":";;;;KAMK,kBAAA,GAAqB,KAAA,CAAM,cAAA;EAC9B,IAAA;EACA,aAAA;EACA,OAAA;EACA,WAAA;EACA,kBAAA;EACA,iBAAA;EACA,YAAA;EACA,eAAA;AAAA;AAAA,iBAmBO,aAAA,CAAA;EACP,IAAA;EACA,SAAA;EACA,aAAA;EACA,OAAA;EACA,WAAA;EACA,kBAAA;EACA,iBAAA;EACA,YAAA;EACA,eAAA;EAAA,GACG;AAAA,GACF,kBAAA,GAAkB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"encrypted-text.d.ts","names":[],"sources":["../src/encrypted-text.tsx"],"mappings":";;;;KAOK,kBAAA,GAAqB,KAAA,CAAM,cAAA;EAC9B,IAAA;EACA,aAAA;EACA,OAAA;EACA,WAAA;EACA,kBAAA;EACA,iBAAA;EACA,YAAA;EACA,eAAA;AAAA;AAAA,iBAmBO,aAAA,CAAA;EACP,IAAA;EACA,SAAA;EACA,aAAA;EACA,OAAA;EACA,WAAA;EACA,kBAAA;EACA,iBAAA;EACA,YAAA;EACA,eAAA;EAAA,GACG;AAAA,GACF,kBAAA,GAAkB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -2,6 +2,7 @@
2
2
  import { cn } from "./lib/utils.js";
3
3
  import * as React from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { useReducedMotion } from "motion/react";
5
6
  //#region src/encrypted-text.tsx
6
7
  const DEFAULT_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-={}[];:,.<>/?";
7
8
  function randomChar(charset) {
@@ -14,6 +15,7 @@ function scramblePreservingSpaces(original, charset) {
14
15
  return result;
15
16
  }
16
17
  function EncryptedText({ text, className, revealDelayMs = 50, charset = DEFAULT_CHARSET, flipDelayMs = 50, encryptedClassName, revealedClassName, scrambleOnly = false, scrambleOneChar = false, ...props }) {
18
+ const prefersReducedMotion = useReducedMotion();
17
19
  const ref = React.useRef(null);
18
20
  const [isInView, setIsInView] = React.useState(false);
19
21
  const [revealCount, setRevealCount] = React.useState(0);
@@ -23,6 +25,7 @@ function EncryptedText({ text, className, revealDelayMs = 50, charset = DEFAULT_
23
25
  const lastFlipTimeRef = React.useRef(0);
24
26
  const scrambleCharsRef = React.useRef(text ? scramblePreservingSpaces(text, charset).split("") : []);
25
27
  React.useEffect(() => {
28
+ if (prefersReducedMotion) return;
26
29
  const el = ref.current;
27
30
  if (!el) return;
28
31
  const observer = new IntersectionObserver(([entry]) => {
@@ -33,8 +36,9 @@ function EncryptedText({ text, className, revealDelayMs = 50, charset = DEFAULT_
33
36
  }, { threshold: 0 });
34
37
  observer.observe(el);
35
38
  return () => observer.disconnect();
36
- }, []);
39
+ }, [prefersReducedMotion]);
37
40
  React.useEffect(() => {
41
+ if (prefersReducedMotion) return;
38
42
  if (!isInView) return;
39
43
  scrambleCharsRef.current = (text ? scramblePreservingSpaces(text, charset) : "").split("");
40
44
  startTimeRef.current = performance.now();
@@ -76,6 +80,7 @@ function EncryptedText({ text, className, revealDelayMs = 50, charset = DEFAULT_
76
80
  if (animationFrameRef.current !== null) cancelAnimationFrame(animationFrameRef.current);
77
81
  };
78
82
  }, [
83
+ prefersReducedMotion,
79
84
  isInView,
80
85
  text,
81
86
  revealDelayMs,
@@ -94,7 +99,7 @@ function EncryptedText({ text, className, revealDelayMs = 50, charset = DEFAULT_
94
99
  className: "sr-only",
95
100
  children: text
96
101
  }), text.split("").map((char, index) => {
97
- const isRevealed = !scrambleOnly && index < revealCount;
102
+ const isRevealed = prefersReducedMotion || !scrambleOnly && index < revealCount;
98
103
  const displayChar = isRevealed ? char : char === " " ? " " : scrambleCharsRef.current[index] ?? randomChar(charset);
99
104
  return /* @__PURE__ */ jsx("span", {
100
105
  "aria-hidden": "true",
@@ -1 +1 @@
1
- {"version":3,"file":"encrypted-text.js","names":[],"sources":["../src/encrypted-text.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"./lib/utils\"\n\ntype EncryptedTextProps = React.ComponentProps<\"span\"> & {\n text: string\n revealDelayMs?: number\n charset?: string\n flipDelayMs?: number\n encryptedClassName?: string\n revealedClassName?: string\n scrambleOnly?: boolean\n scrambleOneChar?: boolean\n}\n\nconst DEFAULT_CHARSET =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-={}[];:,.<>/?\"\n\nfunction randomChar(charset: string): string {\n return charset.charAt(Math.floor(Math.random() * charset.length))\n}\n\nfunction scramblePreservingSpaces(original: string, charset: string): string {\n if (!original) return \"\"\n let result = \"\"\n for (let i = 0; i < original.length; i += 1) {\n result += original[i] === \" \" ? \" \" : randomChar(charset)\n }\n return result\n}\n\nfunction EncryptedText({\n text,\n className,\n revealDelayMs = 50,\n charset = DEFAULT_CHARSET,\n flipDelayMs = 50,\n encryptedClassName,\n revealedClassName,\n scrambleOnly = false,\n scrambleOneChar = false,\n ...props\n}: EncryptedTextProps) {\n const ref = React.useRef<HTMLSpanElement>(null)\n const [isInView, setIsInView] = React.useState(false)\n const [revealCount, setRevealCount] = React.useState(0)\n const [, setFlipTick] = React.useState(0)\n\n const animationFrameRef = React.useRef<number | null>(null)\n const startTimeRef = React.useRef(0)\n const lastFlipTimeRef = React.useRef(0)\n const scrambleCharsRef = React.useRef<string[]>(\n text ? scramblePreservingSpaces(text, charset).split(\"\") : []\n )\n\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry?.isIntersecting) {\n setIsInView(true)\n observer.disconnect()\n }\n },\n { threshold: 0 }\n )\n\n observer.observe(el)\n return () => observer.disconnect()\n }, [])\n\n React.useEffect(() => {\n if (!isInView) return\n\n const initial = text\n ? scramblePreservingSpaces(text, charset)\n : \"\"\n scrambleCharsRef.current = initial.split(\"\")\n startTimeRef.current = performance.now()\n lastFlipTimeRef.current = startTimeRef.current\n setRevealCount(0)\n\n let isCancelled = false\n\n const update = (now: number) => {\n if (isCancelled) return\n\n const totalLength = text.length\n\n if (scrambleOnly) {\n const timeSinceLastFlip = now - lastFlipTimeRef.current\n if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {\n if (scrambleOneChar) {\n const indices: number[] = []\n for (let i = 0; i < totalLength; i++) {\n if (text[i] !== \" \") indices.push(i)\n }\n if (indices.length > 0) {\n const idx = indices[Math.floor(Math.random() * indices.length)]!\n scrambleCharsRef.current[idx] = randomChar(charset)\n }\n } else {\n for (let index = 0; index < totalLength; index += 1) {\n scrambleCharsRef.current[index] =\n text[index] === \" \" ? \" \" : randomChar(charset)\n }\n }\n lastFlipTimeRef.current = now\n setFlipTick((t) => (t + 1) & 0xffff)\n }\n animationFrameRef.current = requestAnimationFrame(update)\n return\n }\n\n const elapsedMs = now - startTimeRef.current\n const currentRevealCount = Math.min(\n totalLength,\n Math.floor(elapsedMs / Math.max(1, revealDelayMs))\n )\n\n setRevealCount(currentRevealCount)\n\n if (currentRevealCount >= totalLength) return\n\n const timeSinceLastFlip = now - lastFlipTimeRef.current\n if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {\n for (let index = currentRevealCount; index < totalLength; index += 1) {\n scrambleCharsRef.current[index] =\n text[index] === \" \" ? \" \" : randomChar(charset)\n }\n lastFlipTimeRef.current = now\n }\n\n animationFrameRef.current = requestAnimationFrame(update)\n }\n\n animationFrameRef.current = requestAnimationFrame(update)\n\n return () => {\n isCancelled = true\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current)\n }\n }\n }, [isInView, text, revealDelayMs, charset, flipDelayMs, scrambleOnly, scrambleOneChar])\n\n if (!text) return null\n\n return (\n <span\n ref={ref}\n data-slot=\"encrypted-text\"\n className={className}\n {...props}\n >\n {/* Real text for assistive tech; the animated glyphs below are decorative\n * (aria-label on a role-less span is unreliable, and the scrambled\n * characters must not be read out). */}\n <span className=\"sr-only\">{text}</span>\n {text.split(\"\").map((char, index) => {\n const isRevealed = !scrambleOnly && index < revealCount\n const displayChar = isRevealed\n ? char\n : char === \" \"\n ? \" \"\n : (scrambleCharsRef.current[index] ?? randomChar(charset))\n\n return (\n <span\n key={index}\n aria-hidden=\"true\"\n data-slot=\"encrypted-text-char\"\n data-revealed={isRevealed || undefined}\n className={cn(isRevealed ? revealedClassName : encryptedClassName)}\n >\n {displayChar}\n </span>\n )\n })}\n </span>\n )\n}\n\nexport { EncryptedText }\n"],"mappings":"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"}
1
+ {"version":3,"file":"encrypted-text.js","names":[],"sources":["../src/encrypted-text.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"./lib/utils\"\n\ntype EncryptedTextProps = React.ComponentProps<\"span\"> & {\n text: string\n revealDelayMs?: number\n charset?: string\n flipDelayMs?: number\n encryptedClassName?: string\n revealedClassName?: string\n scrambleOnly?: boolean\n scrambleOneChar?: boolean\n}\n\nconst DEFAULT_CHARSET =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-={}[];:,.<>/?\"\n\nfunction randomChar(charset: string): string {\n return charset.charAt(Math.floor(Math.random() * charset.length))\n}\n\nfunction scramblePreservingSpaces(original: string, charset: string): string {\n if (!original) return \"\"\n let result = \"\"\n for (let i = 0; i < original.length; i += 1) {\n result += original[i] === \" \" ? \" \" : randomChar(charset)\n }\n return result\n}\n\nfunction EncryptedText({\n text,\n className,\n revealDelayMs = 50,\n charset = DEFAULT_CHARSET,\n flipDelayMs = 50,\n encryptedClassName,\n revealedClassName,\n scrambleOnly = false,\n scrambleOneChar = false,\n ...props\n}: EncryptedTextProps) {\n const prefersReducedMotion = useReducedMotion()\n const ref = React.useRef<HTMLSpanElement>(null)\n const [isInView, setIsInView] = React.useState(false)\n const [revealCount, setRevealCount] = React.useState(0)\n const [, setFlipTick] = React.useState(0)\n\n const animationFrameRef = React.useRef<number | null>(null)\n const startTimeRef = React.useRef(0)\n const lastFlipTimeRef = React.useRef(0)\n const scrambleCharsRef = React.useRef<string[]>(\n text ? scramblePreservingSpaces(text, charset).split(\"\") : []\n )\n\n React.useEffect(() => {\n if (prefersReducedMotion) return\n const el = ref.current\n if (!el) return\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry?.isIntersecting) {\n setIsInView(true)\n observer.disconnect()\n }\n },\n { threshold: 0 }\n )\n\n observer.observe(el)\n return () => observer.disconnect()\n }, [prefersReducedMotion])\n\n React.useEffect(() => {\n if (prefersReducedMotion) return\n if (!isInView) return\n\n const initial = text\n ? scramblePreservingSpaces(text, charset)\n : \"\"\n scrambleCharsRef.current = initial.split(\"\")\n startTimeRef.current = performance.now()\n lastFlipTimeRef.current = startTimeRef.current\n setRevealCount(0)\n\n let isCancelled = false\n\n const update = (now: number) => {\n if (isCancelled) return\n\n const totalLength = text.length\n\n if (scrambleOnly) {\n const timeSinceLastFlip = now - lastFlipTimeRef.current\n if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {\n if (scrambleOneChar) {\n const indices: number[] = []\n for (let i = 0; i < totalLength; i++) {\n if (text[i] !== \" \") indices.push(i)\n }\n if (indices.length > 0) {\n const idx = indices[Math.floor(Math.random() * indices.length)]!\n scrambleCharsRef.current[idx] = randomChar(charset)\n }\n } else {\n for (let index = 0; index < totalLength; index += 1) {\n scrambleCharsRef.current[index] =\n text[index] === \" \" ? \" \" : randomChar(charset)\n }\n }\n lastFlipTimeRef.current = now\n setFlipTick((t) => (t + 1) & 0xffff)\n }\n animationFrameRef.current = requestAnimationFrame(update)\n return\n }\n\n const elapsedMs = now - startTimeRef.current\n const currentRevealCount = Math.min(\n totalLength,\n Math.floor(elapsedMs / Math.max(1, revealDelayMs))\n )\n\n setRevealCount(currentRevealCount)\n\n if (currentRevealCount >= totalLength) return\n\n const timeSinceLastFlip = now - lastFlipTimeRef.current\n if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {\n for (let index = currentRevealCount; index < totalLength; index += 1) {\n scrambleCharsRef.current[index] =\n text[index] === \" \" ? \" \" : randomChar(charset)\n }\n lastFlipTimeRef.current = now\n }\n\n animationFrameRef.current = requestAnimationFrame(update)\n }\n\n animationFrameRef.current = requestAnimationFrame(update)\n\n return () => {\n isCancelled = true\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current)\n }\n }\n }, [prefersReducedMotion, isInView, text, revealDelayMs, charset, flipDelayMs, scrambleOnly, scrambleOneChar])\n\n if (!text) return null\n\n return (\n <span\n ref={ref}\n data-slot=\"encrypted-text\"\n className={className}\n {...props}\n >\n {/* Real text for assistive tech; the animated glyphs below are decorative\n * (aria-label on a role-less span is unreliable, and the scrambled\n * characters must not be read out). */}\n <span className=\"sr-only\">{text}</span>\n {text.split(\"\").map((char, index) => {\n // Reduced motion: skip the scramble entirely and show every\n // character in its final revealed form from the first frame.\n const isRevealed =\n prefersReducedMotion || (!scrambleOnly && index < revealCount)\n const displayChar = isRevealed\n ? char\n : char === \" \"\n ? \" \"\n : (scrambleCharsRef.current[index] ?? randomChar(charset))\n\n return (\n <span\n key={index}\n aria-hidden=\"true\"\n data-slot=\"encrypted-text-char\"\n data-revealed={isRevealed || undefined}\n className={cn(isRevealed ? revealedClassName : encryptedClassName)}\n >\n {displayChar}\n </span>\n )\n })}\n </span>\n )\n}\n\nexport { EncryptedText }\n"],"mappings":"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"}
@@ -18,7 +18,7 @@ declare function InputGroup({
18
18
  ...props
19
19
  }: InputGroupProps): _$react_jsx_runtime0.JSX.Element;
20
20
  declare const inputGroupAddonVariants: (props?: ({
21
- align?: "inline-end" | "inline-start" | "block-end" | "block-start" | null | undefined;
21
+ align?: "inline-start" | "inline-end" | "block-start" | "block-end" | null | undefined;
22
22
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
23
23
  declare function InputGroupAddon({
24
24
  className,
@@ -1 +1 @@
1
- {"version":3,"file":"masonry.d.ts","names":[],"sources":["../src/masonry.tsx"],"mappings":";;;;;KAaK,YAAA,GAAe,KAAA,CAAM,cAAA;EACxB,OAAA;EACA,WAAA;EACA,GAAA;AAAA;AAAA,KAGG,gBAAA,GAAmB,IAAA,CAAK,eAAA;EAC3B,IAAA;EACA,QAAA,GAAW,KAAA,CAAM,SAAA;AAAA;AAAA,iBAGV,OAAA,CAAA;EACP,OAAA;EACA,WAAA;EACA,GAAA;EACA,SAAA;EACA,QAAA;EAAA,GACG;AAAA,GACF,YAAA,GAAY,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBA0NN,WAAA,CAAA;EACP,SAAA;EACA,IAAA;EACA,QAAA;EAAA,GACG;AAAA,GACF,gBAAA,GAAgB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"masonry.d.ts","names":[],"sources":["../src/masonry.tsx"],"mappings":";;;;;KAkBK,YAAA,GAAe,KAAA,CAAM,cAAA;EACxB,OAAA;EACA,WAAA;EACA,GAAA;AAAA;AAAA,KAGG,gBAAA,GAAmB,IAAA,CAAK,eAAA;EAC3B,IAAA;EACA,QAAA,GAAW,KAAA,CAAM,SAAA;AAAA;AAAA,iBAGV,OAAA,CAAA;EACP,OAAA;EACA,WAAA;EACA,GAAA;EACA,SAAA;EACA,QAAA;EAAA,GACG;AAAA,GACF,YAAA,GAAY,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBA0NN,WAAA,CAAA;EACP,SAAA;EACA,IAAA;EACA,QAAA;EAAA,GACG;AAAA,GACF,gBAAA,GAAgB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
package/dist/masonry.js CHANGED
@@ -3,7 +3,7 @@ import { cn } from "./lib/utils.js";
3
3
  import { StarIcon } from "./lib/internal-icons.js";
4
4
  import * as React from "react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
- import { AnimatePresence, motion } from "motion/react";
6
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
7
7
  //#region src/masonry.tsx
8
8
  const STAGGER_STEP = .05;
9
9
  const MasonryStaggerContext = React.createContext(null);
@@ -159,12 +159,17 @@ function FeaturedBadge() {
159
159
  function MasonryItem({ className, span, children, ...props }) {
160
160
  const isSpanned = span != null && span > 1;
161
161
  const getStaggerIndex = React.useContext(MasonryStaggerContext);
162
+ const prefersReducedMotion = useReducedMotion();
162
163
  const [staggerDelay] = React.useState(() => getStaggerIndex ? getStaggerIndex() * STAGGER_STEP : 0);
163
164
  return /* @__PURE__ */ jsxs(motion.div, {
164
165
  "data-slot": "masonry-item",
165
166
  "data-span": isSpanned ? span : void 0,
166
167
  className: cn("relative", className),
167
- initial: {
168
+ initial: prefersReducedMotion ? {
169
+ opacity: 1,
170
+ y: 0,
171
+ filter: "blur(0px)"
172
+ } : {
168
173
  opacity: 0,
169
174
  y: 10,
170
175
  filter: "blur(8px)"
@@ -174,13 +179,13 @@ function MasonryItem({ className, span, children, ...props }) {
174
179
  y: 0,
175
180
  filter: "blur(0px)"
176
181
  },
177
- transition: {
182
+ transition: prefersReducedMotion ? { duration: 0 } : {
178
183
  type: "spring",
179
184
  stiffness: 100,
180
185
  damping: 10,
181
186
  delay: staggerDelay
182
187
  },
183
- exit: {
188
+ exit: prefersReducedMotion ? { opacity: 0 } : {
184
189
  opacity: 0,
185
190
  scale: 1.2,
186
191
  filter: "blur(8px)"
@@ -1 +1 @@
1
- {"version":3,"file":"masonry.js","names":[],"sources":["../src/masonry.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\n\nimport { AnimatePresence, type HTMLMotionProps, motion } from \"motion/react\"\n\nimport { StarIcon } from \"./lib/internal-icons\"\nimport { cn } from \"./lib/utils\"\n\nconst STAGGER_STEP = 0.05 // 50ms between each item's enter animation\n\nconst MasonryStaggerContext = React.createContext<(() => number) | null>(null)\n\ntype MasonryProps = React.ComponentProps<\"div\"> & {\n columns?: number\n columnWidth?: number\n gap?: number\n}\n\ntype MasonryItemProps = Omit<HTMLMotionProps<\"div\">, \"children\"> & {\n span?: number\n children?: React.ReactNode\n}\n\nfunction Masonry({\n columns,\n columnWidth,\n gap = 4,\n className,\n children,\n ...props\n}: MasonryProps) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const rafRef = React.useRef<number>(0)\n const staggerCounterRef = React.useRef(0)\n\n // Reset counter each render so new items in a batch get fresh 0-based indices\n staggerCounterRef.current = 0\n\n const getStaggerIndex = React.useCallback(\n () => staggerCounterRef.current++,\n [],\n )\n\n React.useLayoutEffect(() => {\n const container = containerRef.current\n if (!container) return\n\n const remPx = parseFloat(\n getComputedStyle(document.documentElement).fontSize,\n )\n const gapPx = gap * 0.25 * remPx\n\n function reflow() {\n const allChildren = Array.from(container!.children) as HTMLElement[]\n // Skip exiting items — they keep their position during exit animation\n const items = allChildren.filter((el) => el.dataset.exiting == null)\n\n if (items.length === 0) {\n container!.style.removeProperty(\"height\")\n return\n }\n\n const containerWidth = container!.clientWidth\n if (containerWidth === 0) return\n\n let colCount: number\n if (columns != null) {\n colCount = Math.max(1, columns)\n } else if (columnWidth != null) {\n colCount = Math.max(\n 1,\n Math.floor((containerWidth + gapPx) / (columnWidth + gapPx)),\n )\n } else {\n colCount = Math.max(\n 1,\n Math.floor((containerWidth + gapPx) / (240 + gapPx)),\n )\n }\n\n // Single column: use normal flow with gap\n if (colCount <= 1) {\n container!.style.removeProperty(\"height\")\n container!.style.display = \"flex\"\n container!.style.flexDirection = \"column\"\n container!.style.gap = `${gapPx}px`\n for (const item of items) {\n item.style.removeProperty(\"position\")\n item.style.removeProperty(\"top\")\n item.style.removeProperty(\"left\")\n item.style.removeProperty(\"width\")\n }\n return\n }\n\n // Multi-column: clear single-column styles\n container!.style.removeProperty(\"display\")\n container!.style.removeProperty(\"flex-direction\")\n container!.style.removeProperty(\"gap\")\n\n const colWidth = (containerWidth - (colCount - 1) * gapPx) / colCount\n const columnBottoms = new Array<number>(colCount).fill(0)\n\n // Partition items: spanning (top-pinned) vs regular\n const topItems: { el: HTMLElement; span: number }[] = []\n const regularItems: HTMLElement[] = []\n for (const item of items) {\n const raw = parseInt(item.dataset.span || \"1\", 10)\n if (raw > 1) {\n topItems.push({ el: item, span: Math.min(raw, colCount) })\n } else {\n regularItems.push(item)\n }\n }\n\n // First pass: set width on all items for correct height measurement\n for (const { el, span } of topItems) {\n el.style.position = \"absolute\"\n el.style.width = `${span * colWidth + (span - 1) * gapPx}px`\n }\n for (const item of regularItems) {\n item.style.position = \"absolute\"\n item.style.width = `${colWidth}px`\n }\n\n // Second pass: batch-read heights\n const topHeights: number[] = []\n for (const { el } of topItems) {\n topHeights.push(el.offsetHeight)\n }\n const regularHeights: number[] = []\n for (const item of regularItems) {\n regularHeights.push(item.offsetHeight)\n }\n\n // Third pass: place top items at Y=0, left-to-right\n let nextCol = 0\n for (let i = 0; i < topItems.length; i++) {\n const { el, span } = topItems[i]!\n const s = Math.min(span, colCount - nextCol)\n\n const x = nextCol * (colWidth + gapPx)\n el.style.top = \"0px\"\n el.style.left = `${x}px`\n // Recalculate width if span was clamped\n if (s !== span) {\n el.style.width = `${s * colWidth + (s - 1) * gapPx}px`\n }\n\n const bottom = topHeights[i]! + gapPx\n for (let c = nextCol; c < nextCol + s; c++) {\n columnBottoms[c] = bottom\n }\n\n nextCol += s\n }\n\n // Fourth pass: place regular items in shortest column\n for (let i = 0; i < regularItems.length; i++) {\n let shortestCol = 0\n for (let c = 1; c < colCount; c++) {\n if (columnBottoms[c]! < columnBottoms[shortestCol]!) {\n shortestCol = c\n }\n }\n\n const x = shortestCol * (colWidth + gapPx)\n const y = columnBottoms[shortestCol]!\n\n regularItems[i]!.style.top = `${y}px`\n regularItems[i]!.style.left = `${x}px`\n\n columnBottoms[shortestCol] = y + regularHeights[i]! + gapPx\n }\n\n const maxBottom = Math.max(...columnBottoms) - gapPx\n container!.style.height = `${Math.max(0, maxBottom)}px`\n }\n\n function scheduleReflow() {\n cancelAnimationFrame(rafRef.current)\n rafRef.current = requestAnimationFrame(reflow)\n }\n\n reflow()\n\n const ro = new ResizeObserver(scheduleReflow)\n ro.observe(container)\n\n const mo = new MutationObserver(scheduleReflow)\n mo.observe(container, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"data-exiting\", \"data-span\"],\n })\n\n // Detect image/media loads that change item heights\n container.addEventListener(\"load\", scheduleReflow, true)\n\n return () => {\n cancelAnimationFrame(rafRef.current)\n ro.disconnect()\n mo.disconnect()\n container.removeEventListener(\"load\", scheduleReflow, true)\n container.style.removeProperty(\"height\")\n container.style.removeProperty(\"display\")\n container.style.removeProperty(\"flex-direction\")\n container.style.removeProperty(\"gap\")\n const items = Array.from(container.children) as HTMLElement[]\n for (const item of items) {\n item.style.removeProperty(\"position\")\n item.style.removeProperty(\"top\")\n item.style.removeProperty(\"left\")\n item.style.removeProperty(\"width\")\n }\n }\n }, [columns, columnWidth, gap])\n\n return (\n <MasonryStaggerContext.Provider value={getStaggerIndex}>\n <div\n ref={containerRef}\n data-slot=\"masonry\"\n className={cn(\"relative\", className)}\n {...props}\n >\n <AnimatePresence>\n {children}\n </AnimatePresence>\n </div>\n </MasonryStaggerContext.Provider>\n )\n}\n\nfunction FeaturedBadge() {\n return (\n <span\n data-slot=\"masonry-badge\"\n className=\"absolute top-2 right-2 z-10 flex size-5 items-center justify-center pointer-events-none\"\n role=\"img\"\n aria-label=\"Featured\"\n >\n <StarIcon width={10} height={10} fill=\"currentColor\" aria-hidden />\n </span>\n )\n}\n\nfunction MasonryItem({\n className,\n span,\n children,\n ...props\n}: MasonryItemProps) {\n const isSpanned = span != null && span > 1\n const getStaggerIndex = React.useContext(MasonryStaggerContext)\n\n // Capture stagger index once on mount — useState initializer runs exactly once\n const [staggerDelay] = React.useState(() =>\n getStaggerIndex ? getStaggerIndex() * STAGGER_STEP : 0,\n )\n\n return (\n <motion.div\n data-slot=\"masonry-item\"\n data-span={isSpanned ? span : undefined}\n className={cn(\"relative\", className)}\n initial={{\n opacity: 0,\n y: 10,\n filter: \"blur(8px)\"\n }}\n animate={{\n opacity: 1,\n y: 0,\n filter: \"blur(0px)\"\n }}\n transition={{\n type: \"spring\",\n stiffness: 100,\n damping: 10,\n delay: staggerDelay,\n }}\n exit={{\n opacity: 0,\n scale: 1.2,\n filter: \"blur(8px)\",\n }}\n {...props}\n >\n {children}\n {isSpanned && <FeaturedBadge />}\n </motion.div>\n )\n}\n\nexport { Masonry, MasonryItem }\n"],"mappings":"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"}
1
+ {"version":3,"file":"masonry.js","names":[],"sources":["../src/masonry.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\n\nimport {\n AnimatePresence,\n type HTMLMotionProps,\n motion,\n useReducedMotion,\n} from \"motion/react\"\n\nimport { StarIcon } from \"./lib/internal-icons\"\nimport { cn } from \"./lib/utils\"\n\nconst STAGGER_STEP = 0.05 // 50ms between each item's enter animation\n\nconst MasonryStaggerContext = React.createContext<(() => number) | null>(null)\n\ntype MasonryProps = React.ComponentProps<\"div\"> & {\n columns?: number\n columnWidth?: number\n gap?: number\n}\n\ntype MasonryItemProps = Omit<HTMLMotionProps<\"div\">, \"children\"> & {\n span?: number\n children?: React.ReactNode\n}\n\nfunction Masonry({\n columns,\n columnWidth,\n gap = 4,\n className,\n children,\n ...props\n}: MasonryProps) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const rafRef = React.useRef<number>(0)\n const staggerCounterRef = React.useRef(0)\n\n // Reset counter each render so new items in a batch get fresh 0-based indices\n staggerCounterRef.current = 0\n\n const getStaggerIndex = React.useCallback(\n () => staggerCounterRef.current++,\n [],\n )\n\n React.useLayoutEffect(() => {\n const container = containerRef.current\n if (!container) return\n\n const remPx = parseFloat(\n getComputedStyle(document.documentElement).fontSize,\n )\n const gapPx = gap * 0.25 * remPx\n\n function reflow() {\n const allChildren = Array.from(container!.children) as HTMLElement[]\n // Skip exiting items — they keep their position during exit animation\n const items = allChildren.filter((el) => el.dataset.exiting == null)\n\n if (items.length === 0) {\n container!.style.removeProperty(\"height\")\n return\n }\n\n const containerWidth = container!.clientWidth\n if (containerWidth === 0) return\n\n let colCount: number\n if (columns != null) {\n colCount = Math.max(1, columns)\n } else if (columnWidth != null) {\n colCount = Math.max(\n 1,\n Math.floor((containerWidth + gapPx) / (columnWidth + gapPx)),\n )\n } else {\n colCount = Math.max(\n 1,\n Math.floor((containerWidth + gapPx) / (240 + gapPx)),\n )\n }\n\n // Single column: use normal flow with gap\n if (colCount <= 1) {\n container!.style.removeProperty(\"height\")\n container!.style.display = \"flex\"\n container!.style.flexDirection = \"column\"\n container!.style.gap = `${gapPx}px`\n for (const item of items) {\n item.style.removeProperty(\"position\")\n item.style.removeProperty(\"top\")\n item.style.removeProperty(\"left\")\n item.style.removeProperty(\"width\")\n }\n return\n }\n\n // Multi-column: clear single-column styles\n container!.style.removeProperty(\"display\")\n container!.style.removeProperty(\"flex-direction\")\n container!.style.removeProperty(\"gap\")\n\n const colWidth = (containerWidth - (colCount - 1) * gapPx) / colCount\n const columnBottoms = new Array<number>(colCount).fill(0)\n\n // Partition items: spanning (top-pinned) vs regular\n const topItems: { el: HTMLElement; span: number }[] = []\n const regularItems: HTMLElement[] = []\n for (const item of items) {\n const raw = parseInt(item.dataset.span || \"1\", 10)\n if (raw > 1) {\n topItems.push({ el: item, span: Math.min(raw, colCount) })\n } else {\n regularItems.push(item)\n }\n }\n\n // First pass: set width on all items for correct height measurement\n for (const { el, span } of topItems) {\n el.style.position = \"absolute\"\n el.style.width = `${span * colWidth + (span - 1) * gapPx}px`\n }\n for (const item of regularItems) {\n item.style.position = \"absolute\"\n item.style.width = `${colWidth}px`\n }\n\n // Second pass: batch-read heights\n const topHeights: number[] = []\n for (const { el } of topItems) {\n topHeights.push(el.offsetHeight)\n }\n const regularHeights: number[] = []\n for (const item of regularItems) {\n regularHeights.push(item.offsetHeight)\n }\n\n // Third pass: place top items at Y=0, left-to-right\n let nextCol = 0\n for (let i = 0; i < topItems.length; i++) {\n const { el, span } = topItems[i]!\n const s = Math.min(span, colCount - nextCol)\n\n const x = nextCol * (colWidth + gapPx)\n el.style.top = \"0px\"\n el.style.left = `${x}px`\n // Recalculate width if span was clamped\n if (s !== span) {\n el.style.width = `${s * colWidth + (s - 1) * gapPx}px`\n }\n\n const bottom = topHeights[i]! + gapPx\n for (let c = nextCol; c < nextCol + s; c++) {\n columnBottoms[c] = bottom\n }\n\n nextCol += s\n }\n\n // Fourth pass: place regular items in shortest column\n for (let i = 0; i < regularItems.length; i++) {\n let shortestCol = 0\n for (let c = 1; c < colCount; c++) {\n if (columnBottoms[c]! < columnBottoms[shortestCol]!) {\n shortestCol = c\n }\n }\n\n const x = shortestCol * (colWidth + gapPx)\n const y = columnBottoms[shortestCol]!\n\n regularItems[i]!.style.top = `${y}px`\n regularItems[i]!.style.left = `${x}px`\n\n columnBottoms[shortestCol] = y + regularHeights[i]! + gapPx\n }\n\n const maxBottom = Math.max(...columnBottoms) - gapPx\n container!.style.height = `${Math.max(0, maxBottom)}px`\n }\n\n function scheduleReflow() {\n cancelAnimationFrame(rafRef.current)\n rafRef.current = requestAnimationFrame(reflow)\n }\n\n reflow()\n\n const ro = new ResizeObserver(scheduleReflow)\n ro.observe(container)\n\n const mo = new MutationObserver(scheduleReflow)\n mo.observe(container, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"data-exiting\", \"data-span\"],\n })\n\n // Detect image/media loads that change item heights\n container.addEventListener(\"load\", scheduleReflow, true)\n\n return () => {\n cancelAnimationFrame(rafRef.current)\n ro.disconnect()\n mo.disconnect()\n container.removeEventListener(\"load\", scheduleReflow, true)\n container.style.removeProperty(\"height\")\n container.style.removeProperty(\"display\")\n container.style.removeProperty(\"flex-direction\")\n container.style.removeProperty(\"gap\")\n const items = Array.from(container.children) as HTMLElement[]\n for (const item of items) {\n item.style.removeProperty(\"position\")\n item.style.removeProperty(\"top\")\n item.style.removeProperty(\"left\")\n item.style.removeProperty(\"width\")\n }\n }\n }, [columns, columnWidth, gap])\n\n return (\n <MasonryStaggerContext.Provider value={getStaggerIndex}>\n <div\n ref={containerRef}\n data-slot=\"masonry\"\n className={cn(\"relative\", className)}\n {...props}\n >\n <AnimatePresence>\n {children}\n </AnimatePresence>\n </div>\n </MasonryStaggerContext.Provider>\n )\n}\n\nfunction FeaturedBadge() {\n return (\n <span\n data-slot=\"masonry-badge\"\n className=\"absolute top-2 right-2 z-10 flex size-5 items-center justify-center pointer-events-none\"\n role=\"img\"\n aria-label=\"Featured\"\n >\n <StarIcon width={10} height={10} fill=\"currentColor\" aria-hidden />\n </span>\n )\n}\n\nfunction MasonryItem({\n className,\n span,\n children,\n ...props\n}: MasonryItemProps) {\n const isSpanned = span != null && span > 1\n const getStaggerIndex = React.useContext(MasonryStaggerContext)\n const prefersReducedMotion = useReducedMotion()\n\n // Capture stagger index once on mount — useState initializer runs exactly once\n const [staggerDelay] = React.useState(() =>\n getStaggerIndex ? getStaggerIndex() * STAGGER_STEP : 0,\n )\n\n // Reduced motion: render items in their final resting state with no\n // entrance spring, blur, or stagger — they appear immediately and exit\n // with a plain fade. Same DOM/output as the settled animated state.\n return (\n <motion.div\n data-slot=\"masonry-item\"\n data-span={isSpanned ? span : undefined}\n className={cn(\"relative\", className)}\n initial={\n prefersReducedMotion\n ? { opacity: 1, y: 0, filter: \"blur(0px)\" }\n : {\n opacity: 0,\n y: 10,\n filter: \"blur(8px)\",\n }\n }\n animate={{\n opacity: 1,\n y: 0,\n filter: \"blur(0px)\"\n }}\n transition={\n prefersReducedMotion\n ? { duration: 0 }\n : {\n type: \"spring\",\n stiffness: 100,\n damping: 10,\n delay: staggerDelay,\n }\n }\n exit={\n prefersReducedMotion\n ? { opacity: 0 }\n : {\n opacity: 0,\n scale: 1.2,\n filter: \"blur(8px)\",\n }\n }\n {...props}\n >\n {children}\n {isSpanned && <FeaturedBadge />}\n </motion.div>\n )\n}\n\nexport { Masonry, MasonryItem }\n"],"mappings":"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"}
package/dist/styles.css CHANGED
@@ -640,6 +640,13 @@
640
640
  --offset-sm: 0px; --offset-md: 0px; --offset-lg: 0px;
641
641
  --stagger-sm: 0ms; --stagger-md: 0ms; --stagger-lg: 0ms;
642
642
  }
643
+
644
+ /* Token-driven recipes collapse via the vars above; a standalone keyframe
645
+ * utility that doesn't use those tokens must be stopped explicitly. (The
646
+ * `animate-spin` loader is intentionally left running — it signals activity.) */
647
+ .animate-caret-blink {
648
+ animation: none;
649
+ }
643
650
  }
644
651
 
645
652
  /* ---------------------------------------------------------------------------
package/dist/toast.d.ts CHANGED
@@ -7,7 +7,7 @@ import * as _$_base_ui_react0 from "@base-ui/react";
7
7
  //#region src/toast.d.ts
8
8
  declare const toastRootClass = "bg-elevated text-contrast ring-contrast/10 rounded-md p-4 text-sm shadow-md ring-1 outline-none select-none";
9
9
  declare const toastIconVariants: (props?: ({
10
- type?: "success" | "warning" | "loading" | "info" | "error" | null | undefined;
10
+ type?: "success" | "loading" | "info" | "warning" | "error" | null | undefined;
11
11
  } & _$class_variance_authority_types0.ClassProp) | undefined) => string;
12
12
  declare const toastViewportVariants: (props?: ({
13
13
  position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | null | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"toast.d.ts","names":[],"sources":["../src/toast.tsx"],"mappings":";;;;;;;cAoBM,cAAA;AAAA,cAGA,iBAAA,GAAiB,KAAA;;IAatB,iCAAA,CAAA,SAAA;AAAA,cAEK,qBAAA,GAAqB,KAAA;;IAiB1B,iCAAA,CAAA,SAAA;AAAA,KAMI,SAAA;AAAA,KAEA,YAAA;EACH,KAAA;EACA,WAAA;EACA,IAAA,GAAO,SAAA;EACP,OAAA;EACA,QAAA;EACA,WAAA,GAAc,KAAA,CAAM,wBAAA;EACpB,OAAA;EACA,QAAA;AAAA;AAAA,KAGG,eAAA;AAAA,KAQA,YAAA;EACH,QAAA,GAAW,eAAA;EACX,KAAA;EACA,OAAA;AAAA;AAAA,KAGG,UAAA,GAAa,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,IAAA;AAAA,KACxD,eAAA,GAAkB,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,KAAA;AAAA,KAC7D,qBAAA,GAAwB,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,WAAA;AAAA,KACnE,gBAAA,GAAmB,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,MAAA;AAAA,KAC9D,eAAA,GAAkB,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,KAAA;AAAA,cAM5D,YAAA,EAAkD,iBAAA,CAAtC,YAAA;AAAA,cAEZ,KAAA,IAAK,cAAA,WACiB,YAAA;2BAQD,OAAA,GAAY,IAAA,CAAK,YAAA;yBAGnB,OAAA,GAAY,IAAA,CAAK,YAAA;2BAGf,OAAA,GAAY,IAAA,CAAK,YAAA;wBAGpB,OAAA,GAAY,IAAA,CAAK,YAAA;2BAGd,OAAA,GAAY,IAAA,CAAK,YAAA;;uBAUrB,OAAA,EAAW,OAAA,CAAQ,YAAA;;;iBA2BjC,SAAA,CAAA;EAAY;AAAA;EAAU,IAAA,GAAO,SAAA;AAAA,IAAW,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAiBxC,KAAA,CAAA;EAAQ,SAAA;EAAA,GAAc;AAAA,GAAS,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAoBzC,UAAA,CAAA;EAAa,SAAA;EAAA,GAAc;AAAA,GAAS,eAAA,GAAe,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAUnD,gBAAA,CAAA;EAAmB,SAAA;EAAA,GAAc;AAAA,GAAS,qBAAA,GAAqB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAU/D,WAAA,CAAA;EAAc,SAAA;EAAA,GAAc;AAAA,GAAS,gBAAA,GAAgB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAarD,UAAA,CAAA;EAAa,SAAA;EAAW,QAAA;EAAA,GAAa;AAAA,GAAS,eAAA,GAAe,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBA0K7D,OAAA,CAAA;EAAU,QAAA;EAAU,KAAA;EAAO;AAAA,GAAW,YAAA,GAAY,oBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"toast.d.ts","names":[],"sources":["../src/toast.tsx"],"mappings":";;;;;;;cAoBM,cAAA;AAAA,cAGA,iBAAA,GAAiB,KAAA;;IAatB,iCAAA,CAAA,SAAA;AAAA,cAEK,qBAAA,GAAqB,KAAA;;IAiB1B,iCAAA,CAAA,SAAA;AAAA,KAMI,SAAA;AAAA,KAEA,YAAA;EACH,KAAA;EACA,WAAA;EACA,IAAA,GAAO,SAAA;EACP,OAAA;EACA,QAAA;EACA,WAAA,GAAc,KAAA,CAAM,wBAAA;EACpB,OAAA;EACA,QAAA;AAAA;AAAA,KAGG,eAAA;AAAA,KAQA,YAAA;EACH,QAAA,GAAW,eAAA;EACX,KAAA;EACA,OAAA;AAAA;AAAA,KAGG,UAAA,GAAa,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,IAAA;AAAA,KACxD,eAAA,GAAkB,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,KAAA;AAAA,KAC7D,qBAAA,GAAwB,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,WAAA;AAAA,KACnE,gBAAA,GAAmB,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,MAAA;AAAA,KAC9D,eAAA,GAAkB,KAAA,CAAM,cAAA,QAAsB,OAAA,CAAe,KAAA;AAAA,cAM5D,YAAA,EAAkD,iBAAA,CAAtC,YAAA;AAAA,cAEZ,KAAA,IAAK,cAAA,WACiB,YAAA;2BAQD,OAAA,GAAY,IAAA,CAAK,YAAA;yBAGnB,OAAA,GAAY,IAAA,CAAK,YAAA;2BAGf,OAAA,GAAY,IAAA,CAAK,YAAA;wBAGpB,OAAA,GAAY,IAAA,CAAK,YAAA;2BAGd,OAAA,GAAY,IAAA,CAAK,YAAA;;uBAUrB,OAAA,EAAW,OAAA,CAAQ,YAAA;;;iBA2BjC,SAAA,CAAA;EAAY;AAAA;EAAU,IAAA,GAAO,SAAA;AAAA,IAAW,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAiBxC,KAAA,CAAA;EAAQ,SAAA;EAAA,GAAc;AAAA,GAAS,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAoBzC,UAAA,CAAA;EAAa,SAAA;EAAA,GAAc;AAAA,GAAS,eAAA,GAAe,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAUnD,gBAAA,CAAA;EAAmB,SAAA;EAAA,GAAc;AAAA,GAAS,qBAAA,GAAqB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAU/D,WAAA,CAAA;EAAc,SAAA;EAAA,GAAc;AAAA,GAAS,gBAAA,GAAgB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBAarD,UAAA,CAAA;EAAa,SAAA;EAAW,QAAA;EAAA,GAAa;AAAA,GAAS,eAAA,GAAe,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBA6K7D,OAAA,CAAA;EAAU,QAAA;EAAU,KAAA;EAAO;AAAA,GAAW,YAAA,GAAY,oBAAA,CAAA,GAAA,CAAA,OAAA"}
package/dist/toast.js CHANGED
@@ -148,7 +148,8 @@ const STACKING_BASE = [
148
148
  "data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
149
149
  "data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
150
150
  "data-[limited]:opacity-0",
151
- "[transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,filter_0.5s,height_0.15s]"
151
+ "[transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,filter_0.5s,height_0.15s]",
152
+ "motion-reduce:[transition:none]"
152
153
  ].join(" ");
153
154
  const STACKING_TOP = [
154
155
  "[--toast-stack-offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--toast-gap))+var(--toast-swipe-movement-y))]",
package/dist/toast.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"toast.js","names":["ToastPrimitive"],"sources":["../src/toast.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Toast as ToastPrimitive } from \"@base-ui/react/toast\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"./lib/utils\"\nimport {\n CloseIcon,\n SuccessCircleIcon,\n InfoCircleIcon,\n AlertTriangleIcon,\n ErrorCircleIcon,\n LoaderIcon,\n} from \"./lib/internal-icons\"\n\n// ---------------------------------------------------------------------------\n// Styles\n// ---------------------------------------------------------------------------\n\nconst toastRootClass =\n \"bg-elevated text-contrast ring-contrast/10 rounded-md p-4 text-sm shadow-md ring-1 outline-none select-none\"\n\nconst toastIconVariants = cva(\n \"mt-0.5 size-4 shrink-0\",\n {\n variants: {\n type: {\n loading: \"text-muted animate-spin\",\n success: \"text-success\",\n info: \"text-info\",\n warning: \"text-warning\",\n error: \"text-destructive\",\n },\n },\n }\n)\n\nconst toastViewportVariants = cva(\n \"fixed z-[100] flex w-full outline-none sm:max-w-sm\",\n {\n variants: {\n position: {\n \"top-left\": \"top-4 left-4\",\n \"top-center\": \"top-4 right-0 left-0 mx-auto\",\n \"top-right\": \"top-4 right-4\",\n \"bottom-left\": \"bottom-4 left-4\",\n \"bottom-center\": \"bottom-4 right-0 left-0 mx-auto\",\n \"bottom-right\": \"bottom-4 right-4\",\n },\n },\n defaultVariants: {\n position: \"bottom-right\",\n },\n },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype ToastType = \"loading\" | \"success\" | \"info\" | \"warning\" | \"error\"\n\ntype ToastOptions = {\n title?: string\n description?: string\n type?: ToastType\n timeout?: number\n priority?: \"low\" | \"high\"\n actionProps?: React.ComponentPropsWithoutRef<\"button\">\n onClose?: () => void\n onRemove?: () => void\n}\n\ntype ToasterPosition =\n | \"top-left\"\n | \"top-center\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-center\"\n | \"bottom-right\"\n\ntype ToasterProps = {\n position?: ToasterPosition\n limit?: number\n timeout?: number\n}\n\ntype ToastProps = React.ComponentProps<typeof ToastPrimitive.Root>\ntype ToastTitleProps = React.ComponentProps<typeof ToastPrimitive.Title>\ntype ToastDescriptionProps = React.ComponentProps<typeof ToastPrimitive.Description>\ntype ToastActionProps = React.ComponentProps<typeof ToastPrimitive.Action>\ntype ToastCloseProps = React.ComponentProps<typeof ToastPrimitive.Close>\n\n// ---------------------------------------------------------------------------\n// Toast manager API\n// ---------------------------------------------------------------------------\n\nconst toastManager = ToastPrimitive.createToastManager()\n\nconst toast = Object.assign(\n (titleOrOptions: string | ToastOptions) => {\n const options =\n typeof titleOrOptions === \"string\"\n ? { title: titleOrOptions }\n : titleOrOptions\n return toastManager.add(options)\n },\n {\n success: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"success\", title }),\n\n error: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"error\", title }),\n\n warning: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"warning\", title }),\n\n info: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"info\", title }),\n\n loading: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({\n ...options,\n type: \"loading\",\n title,\n timeout: options?.timeout ?? 0,\n }),\n\n dismiss: (id: string) => toastManager.close(id),\n\n update: (id: string, updates: Partial<ToastOptions>) =>\n toastManager.update(id, updates),\n\n promise: toastManager.promise.bind(toastManager),\n },\n)\n\n// ---------------------------------------------------------------------------\n// Icons\n// ---------------------------------------------------------------------------\n\nconst iconMap = {\n loading: LoaderIcon,\n success: SuccessCircleIcon,\n info: InfoCircleIcon,\n warning: AlertTriangleIcon,\n error: ErrorCircleIcon,\n} as const\n\nconst toastTypeLabels: Record<ToastType, string> = {\n loading: \"Loading\",\n success: \"Success\",\n info: \"Information\",\n warning: \"Warning\",\n error: \"Error\",\n}\n\nfunction ToastIcon({ type }: { type?: ToastType }) {\n if (!type) return null\n const Icon = iconMap[type]\n return (\n <>\n <Icon className={cn(toastIconVariants({ type }))} />\n {/* The icon is decorative (color/shape); announce the type in words so it\n * isn't conveyed by color alone. */}\n <span className=\"sr-only\">{toastTypeLabels[type]}</span>\n </>\n )\n}\n\n// ---------------------------------------------------------------------------\n// Toast primitives\n// ---------------------------------------------------------------------------\n\nfunction Toast({ className, ...props }: ToastProps) {\n return (\n <ToastPrimitive.Root\n data-slot=\"toast\"\n className={cn(toastRootClass, className)}\n {...props}\n />\n )\n}\n\nfunction ToastBody({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"toast-body\"\n className={cn(\"flex flex-1 flex-col gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastTitle({ className, ...props }: ToastTitleProps) {\n return (\n <ToastPrimitive.Title\n data-slot=\"toast-title\"\n className={cn(\"text-sm font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastDescription({ className, ...props }: ToastDescriptionProps) {\n return (\n <ToastPrimitive.Description\n data-slot=\"toast-description\"\n className={cn(\"text-muted text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastAction({ className, ...props }: ToastActionProps) {\n return (\n <ToastPrimitive.Action\n data-slot=\"toast-action\"\n className={cn(\n \"hover:bg-secondary inline-flex h-7 items-center rounded-md border border-line px-2.5 text-xs font-medium transition-colors outline-none focus-visible:border-focus focus-visible:ring-focus/50 focus-visible:ring-3\",\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction ToastClose({ className, children, ...props }: ToastCloseProps) {\n return (\n <ToastPrimitive.Close\n data-slot=\"toast-close\"\n className={cn(\n \"text-muted hover:text-contrast shrink-0 rounded-md p-0.5 transition-colors outline-none focus-visible:ring-focus/50 focus-visible:ring-3\",\n className,\n )}\n {...props}\n >\n {children ?? (\n <>\n <CloseIcon className=\"size-4\" />\n <span className=\"sr-only\">Close</span>\n </>\n )}\n </ToastPrimitive.Close>\n )\n}\n\n// ---------------------------------------------------------------------------\n// 3D stacking\n// ---------------------------------------------------------------------------\n\n// Shared classes for both top and bottom positions\nconst STACKING_BASE = [\n // CSS custom properties\n \"[--toast-gap:0.5rem]\",\n \"[--toast-peek:0.5rem]\",\n \"[--toast-scale:calc(max(0,1-(var(--toast-index)*0.05)))]\",\n \"[--toast-shrink:calc(1-var(--toast-scale))]\",\n \"[--toast-stack-height:var(--toast-frontmost-height,var(--toast-height))]\",\n // Layout\n \"absolute w-full\",\n \"[z-index:calc(1000-var(--toast-index))]\",\n \"h-[var(--toast-stack-height)] data-[expanded]:h-[var(--toast-height)]\",\n // Depth blur — subtle defocus on behind toasts, cleared on expand\n \"[filter:blur(calc(var(--toast-index)*0.4px))]\",\n \"data-[expanded]:[filter:none]\",\n // Expanded (hover)\n \"data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-stack-offset-y)))]\",\n // Exit — shared\n \"data-[ending-style]:opacity-0\",\n \"data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--toast-stack-offset-y))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--toast-stack-offset-y))]\",\n \"data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-[limited]:opacity-0\",\n \"[transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,filter_0.5s,height_0.15s]\",\n].join(\" \")\n\n// Direction-specific classes — sign flips for anchor, offset, transform, enter/exit\nconst STACKING_TOP = [\n \"[--toast-stack-offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--toast-gap))+var(--toast-swipe-movement-y))]\",\n \"top-0 origin-top\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-stack-height))))_scale(var(--toast-scale))]\",\n \"after:absolute after:bottom-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-['']\",\n \"data-[starting-style]:[transform:translateY(-150%)]\",\n \"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)]\",\n].join(\" \")\n\nconst STACKING_BOTTOM = [\n \"[--toast-stack-offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--toast-gap)*-1)+var(--toast-swipe-movement-y))]\",\n \"bottom-0 origin-bottom\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-stack-height))))_scale(var(--toast-scale))]\",\n \"after:absolute after:top-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-['']\",\n \"data-[starting-style]:[transform:translateY(150%)]\",\n \"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]\",\n].join(\" \")\n\nconst STACKING_TOP_CLASS = `${STACKING_BASE} ${STACKING_TOP}`\nconst STACKING_BOTTOM_CLASS = `${STACKING_BASE} ${STACKING_BOTTOM}`\n\n// ---------------------------------------------------------------------------\n// Toaster\n// ---------------------------------------------------------------------------\n\nfunction ToasterContent({ position = \"bottom-right\" }: Pick<ToasterProps, \"position\">) {\n const { toasts } = ToastPrimitive.useToastManager()\n const isTop = position.startsWith(\"top\")\n\n return (\n <ToastPrimitive.Portal>\n <ToastPrimitive.Viewport\n data-slot=\"toast-viewport\"\n className={cn(toastViewportVariants({ position }))}\n >\n {toasts.map((t) => (\n <Toast\n key={t.id}\n toast={t}\n swipeDirection={isTop ? [\"up\", \"right\"] : [\"down\", \"right\"]}\n className={isTop ? STACKING_TOP_CLASS : STACKING_BOTTOM_CLASS}\n >\n <ToastPrimitive.Content\n data-slot=\"toast-content\"\n className=\"flex items-start gap-3 overflow-hidden transition-opacity duration-200 data-[behind]:pointer-events-none data-[behind]:opacity-0 data-[expanded]:pointer-events-auto data-[expanded]:opacity-100\"\n >\n <ToastIcon type={t.type as ToastType | undefined} />\n\n <ToastBody>\n {t.title && <ToastTitle>{t.title}</ToastTitle>}\n {t.description && (\n <ToastDescription>{t.description}</ToastDescription>\n )}\n {t.actionProps && (\n <div className=\"mt-1.5\">\n <ToastAction {...t.actionProps} />\n </div>\n )}\n </ToastBody>\n\n <ToastClose />\n </ToastPrimitive.Content>\n </Toast>\n ))}\n </ToastPrimitive.Viewport>\n </ToastPrimitive.Portal>\n )\n}\n\n// The toast manager is a module-level singleton, so every mounted <Toaster/>\n// renders the SAME shared toast list — duplicating every toast. This registry\n// elects a single primary Toaster (the first mounted); the rest render null.\n// Guards against a consumer accidentally rendering two, and against Storybook\n// autodocs rendering several stories — each with a Toaster — on one page.\nconst mountedToasters: symbol[] = []\nconst toasterListeners = new Set<() => void>()\n\nfunction notifyToasters() {\n for (const listener of toasterListeners) {\n listener()\n }\n}\n\nfunction useIsPrimaryToaster() {\n const keyRef = React.useRef<symbol | null>(null)\n if (keyRef.current === null) {\n keyRef.current = Symbol(\"toaster\")\n }\n const key = keyRef.current\n\n const isPrimary = React.useSyncExternalStore(\n React.useCallback((onChange: () => void) => {\n toasterListeners.add(onChange)\n return () => {\n toasterListeners.delete(onChange)\n }\n }, []),\n () => mountedToasters[0] === key,\n () => false,\n )\n\n React.useEffect(() => {\n mountedToasters.push(key)\n notifyToasters()\n return () => {\n const index = mountedToasters.indexOf(key)\n if (index !== -1) {\n mountedToasters.splice(index, 1)\n }\n notifyToasters()\n }\n }, [key])\n\n return isPrimary\n}\n\nfunction Toaster({ position, limit, timeout }: ToasterProps) {\n const isPrimary = useIsPrimaryToaster()\n if (!isPrimary) {\n return null\n }\n return (\n <ToastPrimitive.Provider\n toastManager={toastManager}\n limit={limit}\n timeout={timeout}\n >\n <ToasterContent position={position} />\n </ToastPrimitive.Provider>\n )\n}\n\nexport {\n toastRootClass,\n toastIconVariants,\n toastViewportVariants,\n toastManager,\n toast,\n Toaster,\n Toast,\n ToastIcon,\n ToastTitle,\n ToastDescription,\n ToastAction,\n ToastClose,\n}\n\nexport type {\n ToastType,\n ToastOptions,\n ToasterPosition,\n ToasterProps\n}"],"mappings":"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"}
1
+ {"version":3,"file":"toast.js","names":["ToastPrimitive"],"sources":["../src/toast.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Toast as ToastPrimitive } from \"@base-ui/react/toast\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"./lib/utils\"\nimport {\n CloseIcon,\n SuccessCircleIcon,\n InfoCircleIcon,\n AlertTriangleIcon,\n ErrorCircleIcon,\n LoaderIcon,\n} from \"./lib/internal-icons\"\n\n// ---------------------------------------------------------------------------\n// Styles\n// ---------------------------------------------------------------------------\n\nconst toastRootClass =\n \"bg-elevated text-contrast ring-contrast/10 rounded-md p-4 text-sm shadow-md ring-1 outline-none select-none\"\n\nconst toastIconVariants = cva(\n \"mt-0.5 size-4 shrink-0\",\n {\n variants: {\n type: {\n loading: \"text-muted animate-spin\",\n success: \"text-success\",\n info: \"text-info\",\n warning: \"text-warning\",\n error: \"text-destructive\",\n },\n },\n }\n)\n\nconst toastViewportVariants = cva(\n \"fixed z-[100] flex w-full outline-none sm:max-w-sm\",\n {\n variants: {\n position: {\n \"top-left\": \"top-4 left-4\",\n \"top-center\": \"top-4 right-0 left-0 mx-auto\",\n \"top-right\": \"top-4 right-4\",\n \"bottom-left\": \"bottom-4 left-4\",\n \"bottom-center\": \"bottom-4 right-0 left-0 mx-auto\",\n \"bottom-right\": \"bottom-4 right-4\",\n },\n },\n defaultVariants: {\n position: \"bottom-right\",\n },\n },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype ToastType = \"loading\" | \"success\" | \"info\" | \"warning\" | \"error\"\n\ntype ToastOptions = {\n title?: string\n description?: string\n type?: ToastType\n timeout?: number\n priority?: \"low\" | \"high\"\n actionProps?: React.ComponentPropsWithoutRef<\"button\">\n onClose?: () => void\n onRemove?: () => void\n}\n\ntype ToasterPosition =\n | \"top-left\"\n | \"top-center\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-center\"\n | \"bottom-right\"\n\ntype ToasterProps = {\n position?: ToasterPosition\n limit?: number\n timeout?: number\n}\n\ntype ToastProps = React.ComponentProps<typeof ToastPrimitive.Root>\ntype ToastTitleProps = React.ComponentProps<typeof ToastPrimitive.Title>\ntype ToastDescriptionProps = React.ComponentProps<typeof ToastPrimitive.Description>\ntype ToastActionProps = React.ComponentProps<typeof ToastPrimitive.Action>\ntype ToastCloseProps = React.ComponentProps<typeof ToastPrimitive.Close>\n\n// ---------------------------------------------------------------------------\n// Toast manager API\n// ---------------------------------------------------------------------------\n\nconst toastManager = ToastPrimitive.createToastManager()\n\nconst toast = Object.assign(\n (titleOrOptions: string | ToastOptions) => {\n const options =\n typeof titleOrOptions === \"string\"\n ? { title: titleOrOptions }\n : titleOrOptions\n return toastManager.add(options)\n },\n {\n success: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"success\", title }),\n\n error: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"error\", title }),\n\n warning: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"warning\", title }),\n\n info: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"info\", title }),\n\n loading: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({\n ...options,\n type: \"loading\",\n title,\n timeout: options?.timeout ?? 0,\n }),\n\n dismiss: (id: string) => toastManager.close(id),\n\n update: (id: string, updates: Partial<ToastOptions>) =>\n toastManager.update(id, updates),\n\n promise: toastManager.promise.bind(toastManager),\n },\n)\n\n// ---------------------------------------------------------------------------\n// Icons\n// ---------------------------------------------------------------------------\n\nconst iconMap = {\n loading: LoaderIcon,\n success: SuccessCircleIcon,\n info: InfoCircleIcon,\n warning: AlertTriangleIcon,\n error: ErrorCircleIcon,\n} as const\n\nconst toastTypeLabels: Record<ToastType, string> = {\n loading: \"Loading\",\n success: \"Success\",\n info: \"Information\",\n warning: \"Warning\",\n error: \"Error\",\n}\n\nfunction ToastIcon({ type }: { type?: ToastType }) {\n if (!type) return null\n const Icon = iconMap[type]\n return (\n <>\n <Icon className={cn(toastIconVariants({ type }))} />\n {/* The icon is decorative (color/shape); announce the type in words so it\n * isn't conveyed by color alone. */}\n <span className=\"sr-only\">{toastTypeLabels[type]}</span>\n </>\n )\n}\n\n// ---------------------------------------------------------------------------\n// Toast primitives\n// ---------------------------------------------------------------------------\n\nfunction Toast({ className, ...props }: ToastProps) {\n return (\n <ToastPrimitive.Root\n data-slot=\"toast\"\n className={cn(toastRootClass, className)}\n {...props}\n />\n )\n}\n\nfunction ToastBody({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"toast-body\"\n className={cn(\"flex flex-1 flex-col gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastTitle({ className, ...props }: ToastTitleProps) {\n return (\n <ToastPrimitive.Title\n data-slot=\"toast-title\"\n className={cn(\"text-sm font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastDescription({ className, ...props }: ToastDescriptionProps) {\n return (\n <ToastPrimitive.Description\n data-slot=\"toast-description\"\n className={cn(\"text-muted text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastAction({ className, ...props }: ToastActionProps) {\n return (\n <ToastPrimitive.Action\n data-slot=\"toast-action\"\n className={cn(\n \"hover:bg-secondary inline-flex h-7 items-center rounded-md border border-line px-2.5 text-xs font-medium transition-colors outline-none focus-visible:border-focus focus-visible:ring-focus/50 focus-visible:ring-3\",\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction ToastClose({ className, children, ...props }: ToastCloseProps) {\n return (\n <ToastPrimitive.Close\n data-slot=\"toast-close\"\n className={cn(\n \"text-muted hover:text-contrast shrink-0 rounded-md p-0.5 transition-colors outline-none focus-visible:ring-focus/50 focus-visible:ring-3\",\n className,\n )}\n {...props}\n >\n {children ?? (\n <>\n <CloseIcon className=\"size-4\" />\n <span className=\"sr-only\">Close</span>\n </>\n )}\n </ToastPrimitive.Close>\n )\n}\n\n// ---------------------------------------------------------------------------\n// 3D stacking\n// ---------------------------------------------------------------------------\n\n// Shared classes for both top and bottom positions\nconst STACKING_BASE = [\n // CSS custom properties\n \"[--toast-gap:0.5rem]\",\n \"[--toast-peek:0.5rem]\",\n \"[--toast-scale:calc(max(0,1-(var(--toast-index)*0.05)))]\",\n \"[--toast-shrink:calc(1-var(--toast-scale))]\",\n \"[--toast-stack-height:var(--toast-frontmost-height,var(--toast-height))]\",\n // Layout\n \"absolute w-full\",\n \"[z-index:calc(1000-var(--toast-index))]\",\n \"h-[var(--toast-stack-height)] data-[expanded]:h-[var(--toast-height)]\",\n // Depth blur — subtle defocus on behind toasts, cleared on expand\n \"[filter:blur(calc(var(--toast-index)*0.4px))]\",\n \"data-[expanded]:[filter:none]\",\n // Expanded (hover)\n \"data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-stack-offset-y)))]\",\n // Exit — shared\n \"data-[ending-style]:opacity-0\",\n \"data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--toast-stack-offset-y))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--toast-stack-offset-y))]\",\n \"data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-[limited]:opacity-0\",\n \"[transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,filter_0.5s,height_0.15s]\",\n // Collapse the stacking motion under reduced motion — toasts appear and\n // restack instantly instead of sliding/scaling.\n \"motion-reduce:[transition:none]\",\n].join(\" \")\n\n// Direction-specific classes — sign flips for anchor, offset, transform, enter/exit\nconst STACKING_TOP = [\n \"[--toast-stack-offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--toast-gap))+var(--toast-swipe-movement-y))]\",\n \"top-0 origin-top\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-stack-height))))_scale(var(--toast-scale))]\",\n \"after:absolute after:bottom-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-['']\",\n \"data-[starting-style]:[transform:translateY(-150%)]\",\n \"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)]\",\n].join(\" \")\n\nconst STACKING_BOTTOM = [\n \"[--toast-stack-offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--toast-gap)*-1)+var(--toast-swipe-movement-y))]\",\n \"bottom-0 origin-bottom\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-stack-height))))_scale(var(--toast-scale))]\",\n \"after:absolute after:top-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-['']\",\n \"data-[starting-style]:[transform:translateY(150%)]\",\n \"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]\",\n].join(\" \")\n\nconst STACKING_TOP_CLASS = `${STACKING_BASE} ${STACKING_TOP}`\nconst STACKING_BOTTOM_CLASS = `${STACKING_BASE} ${STACKING_BOTTOM}`\n\n// ---------------------------------------------------------------------------\n// Toaster\n// ---------------------------------------------------------------------------\n\nfunction ToasterContent({ position = \"bottom-right\" }: Pick<ToasterProps, \"position\">) {\n const { toasts } = ToastPrimitive.useToastManager()\n const isTop = position.startsWith(\"top\")\n\n return (\n <ToastPrimitive.Portal>\n <ToastPrimitive.Viewport\n data-slot=\"toast-viewport\"\n className={cn(toastViewportVariants({ position }))}\n >\n {toasts.map((t) => (\n <Toast\n key={t.id}\n toast={t}\n swipeDirection={isTop ? [\"up\", \"right\"] : [\"down\", \"right\"]}\n className={isTop ? STACKING_TOP_CLASS : STACKING_BOTTOM_CLASS}\n >\n <ToastPrimitive.Content\n data-slot=\"toast-content\"\n className=\"flex items-start gap-3 overflow-hidden transition-opacity duration-200 data-[behind]:pointer-events-none data-[behind]:opacity-0 data-[expanded]:pointer-events-auto data-[expanded]:opacity-100\"\n >\n <ToastIcon type={t.type as ToastType | undefined} />\n\n <ToastBody>\n {t.title && <ToastTitle>{t.title}</ToastTitle>}\n {t.description && (\n <ToastDescription>{t.description}</ToastDescription>\n )}\n {t.actionProps && (\n <div className=\"mt-1.5\">\n <ToastAction {...t.actionProps} />\n </div>\n )}\n </ToastBody>\n\n <ToastClose />\n </ToastPrimitive.Content>\n </Toast>\n ))}\n </ToastPrimitive.Viewport>\n </ToastPrimitive.Portal>\n )\n}\n\n// The toast manager is a module-level singleton, so every mounted <Toaster/>\n// renders the SAME shared toast list — duplicating every toast. This registry\n// elects a single primary Toaster (the first mounted); the rest render null.\n// Guards against a consumer accidentally rendering two, and against Storybook\n// autodocs rendering several stories — each with a Toaster — on one page.\nconst mountedToasters: symbol[] = []\nconst toasterListeners = new Set<() => void>()\n\nfunction notifyToasters() {\n for (const listener of toasterListeners) {\n listener()\n }\n}\n\nfunction useIsPrimaryToaster() {\n const keyRef = React.useRef<symbol | null>(null)\n if (keyRef.current === null) {\n keyRef.current = Symbol(\"toaster\")\n }\n const key = keyRef.current\n\n const isPrimary = React.useSyncExternalStore(\n React.useCallback((onChange: () => void) => {\n toasterListeners.add(onChange)\n return () => {\n toasterListeners.delete(onChange)\n }\n }, []),\n () => mountedToasters[0] === key,\n () => false,\n )\n\n React.useEffect(() => {\n mountedToasters.push(key)\n notifyToasters()\n return () => {\n const index = mountedToasters.indexOf(key)\n if (index !== -1) {\n mountedToasters.splice(index, 1)\n }\n notifyToasters()\n }\n }, [key])\n\n return isPrimary\n}\n\nfunction Toaster({ position, limit, timeout }: ToasterProps) {\n const isPrimary = useIsPrimaryToaster()\n if (!isPrimary) {\n return null\n }\n return (\n <ToastPrimitive.Provider\n toastManager={toastManager}\n limit={limit}\n timeout={timeout}\n >\n <ToasterContent position={position} />\n </ToastPrimitive.Provider>\n )\n}\n\nexport {\n toastRootClass,\n toastIconVariants,\n toastViewportVariants,\n toastManager,\n toast,\n Toaster,\n Toast,\n ToastIcon,\n ToastTitle,\n ToastDescription,\n ToastAction,\n ToastClose,\n}\n\nexport type {\n ToastType,\n ToastOptions,\n ToasterPosition,\n ToasterProps\n}"],"mappings":";;;;;;;;AAoBA,MAAM,iBACJ;AAEF,MAAM,oBAAoB,IACxB,0BACA,EACE,UAAU,EACR,MAAM;CACJ,SAAS;CACT,SAAS;CACT,MAAM;CACN,SAAS;CACT,OAAO;CACR,EACF,EACF,CACF;AAED,MAAM,wBAAwB,IAC5B,sDACA;CACE,UAAU,EACR,UAAU;EACR,YAAY;EACZ,cAAc;EACd,aAAa;EACb,eAAe;EACf,iBAAiB;EACjB,gBAAgB;EACjB,EACF;CACD,iBAAiB,EACf,UAAU,gBACX;CACF,CACF;AA2CD,MAAM,eAAeA,QAAe,oBAAoB;AAExD,MAAM,QAAQ,OAAO,QAClB,mBAA0C;CACzC,MAAM,UACJ,OAAO,mBAAmB,WACtB,EAAE,OAAO,gBAAgB,GACzB;AACN,QAAO,aAAa,IAAI,QAAQ;GAElC;CACE,UAAU,OAAe,YACvB,aAAa,IAAI;EAAE,GAAG;EAAS,MAAM;EAAW;EAAO,CAAC;CAE1D,QAAQ,OAAe,YACrB,aAAa,IAAI;EAAE,GAAG;EAAS,MAAM;EAAS;EAAO,CAAC;CAExD,UAAU,OAAe,YACvB,aAAa,IAAI;EAAE,GAAG;EAAS,MAAM;EAAW;EAAO,CAAC;CAE1D,OAAO,OAAe,YACpB,aAAa,IAAI;EAAE,GAAG;EAAS,MAAM;EAAQ;EAAO,CAAC;CAEvD,UAAU,OAAe,YACvB,aAAa,IAAI;EACf,GAAG;EACH,MAAM;EACN;EACA,SAAS,SAAS,WAAW;EAC9B,CAAC;CAEJ,UAAU,OAAe,aAAa,MAAM,GAAG;CAE/C,SAAS,IAAY,YACnB,aAAa,OAAO,IAAI,QAAQ;CAElC,SAAS,aAAa,QAAQ,KAAK,aAAa;CACjD,CACF;AAMD,MAAM,UAAU;CACd,SAAS;CACT,SAAS;CACT,MAAM;CACN,SAAS;CACT,OAAO;CACR;AAED,MAAM,kBAA6C;CACjD,SAAS;CACT,SAAS;CACT,MAAM;CACN,SAAS;CACT,OAAO;CACR;AAED,SAAS,UAAU,EAAE,QAA8B;AACjD,KAAI,CAAC,KAAM,QAAO;CAClB,MAAM,OAAO,QAAQ;AACrB,QACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,MAAD,EAAM,WAAW,GAAG,kBAAkB,EAAE,MAAM,CAAC,CAAC,EAAI,CAAA,EAGpD,oBAAC,QAAD;EAAM,WAAU;YAAW,gBAAgB;EAAa,CAAA,CACvD,EAAA,CAAA;;AAQP,SAAS,MAAM,EAAE,WAAW,GAAG,SAAqB;AAClD,QACE,oBAACA,QAAe,MAAhB;EACE,aAAU;EACV,WAAW,GAAG,gBAAgB,UAAU;EACxC,GAAI;EACJ,CAAA;;AAIN,SAAS,UAAU,EAAE,WAAW,GAAG,SAAsC;AACvE,QACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,8BAA8B,UAAU;EACtD,GAAI;EACJ,CAAA;;AAIN,SAAS,WAAW,EAAE,WAAW,GAAG,SAA0B;AAC5D,QACE,oBAACA,QAAe,OAAhB;EACE,aAAU;EACV,WAAW,GAAG,uBAAuB,UAAU;EAC/C,GAAI;EACJ,CAAA;;AAIN,SAAS,iBAAiB,EAAE,WAAW,GAAG,SAAgC;AACxE,QACE,oBAACA,QAAe,aAAhB;EACE,aAAU;EACV,WAAW,GAAG,sBAAsB,UAAU;EAC9C,GAAI;EACJ,CAAA;;AAIN,SAAS,YAAY,EAAE,WAAW,GAAG,SAA2B;AAC9D,QACE,oBAACA,QAAe,QAAhB;EACE,aAAU;EACV,WAAW,GACT,uNACA,UACD;EACD,GAAI;EACJ,CAAA;;AAIN,SAAS,WAAW,EAAE,WAAW,UAAU,GAAG,SAA0B;AACtE,QACE,oBAACA,QAAe,OAAhB;EACE,aAAU;EACV,WAAW,GACT,4IACA,UACD;EACD,GAAI;YAEH,YACC,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,WAAD,EAAW,WAAU,UAAW,CAAA,EAChC,oBAAC,QAAD;GAAM,WAAU;aAAU;GAAY,CAAA,CACrC,EAAA,CAAA;EAEgB,CAAA;;AAS3B,MAAM,gBAAgB;CAEpB;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAEA;CACA;CAEA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACD,CAAC,KAAK,IAAI;AAGX,MAAM,eAAe;CACnB;CACA;CACA;CACA;CACA;CACA;CACD,CAAC,KAAK,IAAI;AAEX,MAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACD,CAAC,KAAK,IAAI;AAEX,MAAM,qBAAqB,GAAG,cAAc,GAAG;AAC/C,MAAM,wBAAwB,GAAG,cAAc,GAAG;AAMlD,SAAS,eAAe,EAAE,WAAW,kBAAkD;CACrF,MAAM,EAAE,WAAWA,QAAe,iBAAiB;CACnD,MAAM,QAAQ,SAAS,WAAW,MAAM;AAExC,QACE,oBAACA,QAAe,QAAhB,EAAA,UACE,oBAACA,QAAe,UAAhB;EACE,aAAU;EACV,WAAW,GAAG,sBAAsB,EAAE,UAAU,CAAC,CAAC;YAEjD,OAAO,KAAK,MACX,oBAAC,OAAD;GAEE,OAAO;GACP,gBAAgB,QAAQ,CAAC,MAAM,QAAQ,GAAG,CAAC,QAAQ,QAAQ;GAC3D,WAAW,QAAQ,qBAAqB;aAExC,qBAACA,QAAe,SAAhB;IACE,aAAU;IACV,WAAU;cAFZ;KAIE,oBAAC,WAAD,EAAW,MAAM,EAAE,MAAiC,CAAA;KAEpD,qBAAC,WAAD,EAAA,UAAA;MACG,EAAE,SAAS,oBAAC,YAAD,EAAA,UAAa,EAAE,OAAmB,CAAA;MAC7C,EAAE,eACD,oBAAC,kBAAD,EAAA,UAAmB,EAAE,aAA+B,CAAA;MAErD,EAAE,eACD,oBAAC,OAAD;OAAK,WAAU;iBACb,oBAAC,aAAD,EAAa,GAAI,EAAE,aAAe,CAAA;OAC9B,CAAA;MAEE,EAAA,CAAA;KAEZ,oBAAC,YAAD,EAAc,CAAA;KACS;;GACnB,EAzBD,EAAE,GAyBD,CACR;EACsB,CAAA,EACJ,CAAA;;AAS5B,MAAM,kBAA4B,EAAE;AACpC,MAAM,mCAAmB,IAAI,KAAiB;AAE9C,SAAS,iBAAiB;AACxB,MAAK,MAAM,YAAY,iBACrB,WAAU;;AAId,SAAS,sBAAsB;CAC7B,MAAM,SAAS,MAAM,OAAsB,KAAK;AAChD,KAAI,OAAO,YAAY,KACrB,QAAO,UAAU,OAAO,UAAU;CAEpC,MAAM,MAAM,OAAO;CAEnB,MAAM,YAAY,MAAM,qBACtB,MAAM,aAAa,aAAyB;AAC1C,mBAAiB,IAAI,SAAS;AAC9B,eAAa;AACX,oBAAiB,OAAO,SAAS;;IAElC,EAAE,CAAC,QACA,gBAAgB,OAAO,WACvB,MACP;AAED,OAAM,gBAAgB;AACpB,kBAAgB,KAAK,IAAI;AACzB,kBAAgB;AAChB,eAAa;GACX,MAAM,QAAQ,gBAAgB,QAAQ,IAAI;AAC1C,OAAI,UAAU,GACZ,iBAAgB,OAAO,OAAO,EAAE;AAElC,mBAAgB;;IAEjB,CAAC,IAAI,CAAC;AAET,QAAO;;AAGT,SAAS,QAAQ,EAAE,UAAU,OAAO,WAAyB;AAE3D,KAAI,CADc,qBACJ,CACZ,QAAO;AAET,QACE,oBAACA,QAAe,UAAhB;EACgB;EACP;EACE;YAET,oBAAC,gBAAD,EAA0B,UAAY,CAAA;EACd,CAAA"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cloneElement, isValidElement, useEffect, useId, useRef, useState } from "react";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import { useInView } from "motion/react";
4
+ import { useInView, useReducedMotion } from "motion/react";
5
5
  //#region src/typewriter.tsx
6
6
  function mergeRef(internalRef, externalRef) {
7
7
  return (el) => {
@@ -105,22 +105,24 @@ function Typewriter({ text, children, speed = .04, delay = 0, cursor = true, cur
105
105
  });
106
106
  }
107
107
  function TypingReveal({ text, children, elementRef, id, speed, delay, cursor, cursorChar, shouldAnimate }) {
108
+ const prefersReducedMotion = useReducedMotion();
108
109
  const flat = flattenText(text);
109
110
  const segments = typeof text === "string" ? [{ text }] : text;
110
111
  const [charIndex, setCharIndex] = useState(0);
111
112
  const [started, setStarted] = useState(false);
112
113
  const [done, setDone] = useState(false);
113
114
  useEffect(() => {
114
- if (!shouldAnimate || started) return;
115
+ if (prefersReducedMotion || !shouldAnimate || started) return;
115
116
  const timer = setTimeout(() => setStarted(true), delay * 1e3);
116
117
  return () => clearTimeout(timer);
117
118
  }, [
119
+ prefersReducedMotion,
118
120
  shouldAnimate,
119
121
  delay,
120
122
  started
121
123
  ]);
122
124
  useEffect(() => {
123
- if (!started || done) return;
125
+ if (prefersReducedMotion || !started || done) return;
124
126
  if (charIndex >= flat.length) {
125
127
  setDone(true);
126
128
  return;
@@ -128,6 +130,7 @@ function TypingReveal({ text, children, elementRef, id, speed, delay, cursor, cu
128
130
  const timer = setTimeout(() => setCharIndex((prev) => prev + 1), speed * 1e3);
129
131
  return () => clearTimeout(timer);
130
132
  }, [
133
+ prefersReducedMotion,
131
134
  started,
132
135
  charIndex,
133
136
  flat.length,
@@ -136,8 +139,8 @@ function TypingReveal({ text, children, elementRef, id, speed, delay, cursor, cu
136
139
  ]);
137
140
  if (!isValidElement(children)) return children;
138
141
  const existingRef = children.props.ref;
139
- const content = started ? buildSegmentContent(segments, charIndex) : null;
140
- const showCursor = cursor && started && !done;
142
+ const content = prefersReducedMotion ? buildSegmentContent(segments, flat.length) : started ? buildSegmentContent(segments, charIndex) : null;
143
+ const showCursor = !prefersReducedMotion && cursor && started && !done;
141
144
  return cloneElement(children, {
142
145
  ref: mergeRef(elementRef, existingRef),
143
146
  children: /* @__PURE__ */ jsxs(Fragment, { children: [content, showCursor && /* @__PURE__ */ jsx(BlinkingCursor, {
@@ -147,29 +150,35 @@ function TypingReveal({ text, children, elementRef, id, speed, delay, cursor, cu
147
150
  });
148
151
  }
149
152
  function SmoothReveal({ text, children, elementRef, id, delay, duration, cursor, cursorChar, shouldAnimate }) {
153
+ const prefersReducedMotion = useReducedMotion();
150
154
  const segments = typeof text === "string" ? [{ text }] : text;
151
155
  const [revealed, setRevealed] = useState(false);
152
156
  const [done, setDone] = useState(false);
153
157
  useEffect(() => {
154
- if (!shouldAnimate || revealed) return;
158
+ if (prefersReducedMotion || !shouldAnimate || revealed) return;
155
159
  const timer = setTimeout(() => setRevealed(true), delay * 1e3);
156
160
  return () => clearTimeout(timer);
157
161
  }, [
162
+ prefersReducedMotion,
158
163
  shouldAnimate,
159
164
  delay,
160
165
  revealed
161
166
  ]);
162
167
  useEffect(() => {
163
- if (!revealed) return;
168
+ if (prefersReducedMotion || !revealed) return;
164
169
  const timer = setTimeout(() => setDone(true), duration * 1e3);
165
170
  return () => clearTimeout(timer);
166
- }, [revealed, duration]);
171
+ }, [
172
+ prefersReducedMotion,
173
+ revealed,
174
+ duration
175
+ ]);
167
176
  if (!isValidElement(children)) return children;
168
177
  const childProps = children.props;
169
178
  const existingRef = childProps.ref;
170
179
  const existingStyle = childProps.style ?? {};
171
180
  const animName = `tw-smooth-${id}`;
172
- const showCursor = cursor && revealed && !done;
181
+ const showCursor = !prefersReducedMotion && cursor && revealed && !done;
173
182
  const fullContent = segments.map((seg, i) => seg.className ? /* @__PURE__ */ jsx("span", {
174
183
  className: seg.className,
175
184
  children: seg.text
@@ -180,7 +189,7 @@ function SmoothReveal({ text, children, elementRef, id, delay, duration, cursor,
180
189
  ...existingStyle,
181
190
  whiteSpace: "nowrap",
182
191
  overflow: "hidden",
183
- ...revealed ? { animation: `${animName} ${duration}s linear forwards` } : { maxWidth: 0 }
192
+ ...prefersReducedMotion ? { maxWidth: "100%" } : revealed ? { animation: `${animName} ${duration}s linear forwards` } : { maxWidth: 0 }
184
193
  },
185
194
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
186
195
  /* @__PURE__ */ jsx("style", { children: `@keyframes ${animName} { from { max-width: 0; } to { max-width: 100%; } }` }),
@@ -1 +1 @@
1
- {"version":3,"file":"typewriter.js","names":[],"sources":["../src/typewriter.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n type CSSProperties,\n type ReactElement,\n type Ref,\n cloneElement,\n isValidElement,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\"\nimport { useInView } from \"motion/react\"\n\n// ── Types ────────────────────────────────────────────────────────────\n\ninterface TextSegment {\n text: string\n /** Optional className applied to this segment (e.g., bold, colored) */\n className?: string\n}\n\ninterface TypewriterProps {\n /** Simple string or styled segments for per-word/phrase styling */\n text: string | TextSegment[]\n /** Element to render into. Receives the animated text as children. */\n children: ReactElement\n /** Time per character in seconds. Default: 0.04 */\n speed?: number\n /** Delay before typing starts in seconds. Default: 0 */\n delay?: number\n /** Show a blinking cursor during typing. Default: true */\n cursor?: boolean\n /** Cursor character. Default: '|' */\n cursorChar?: string\n /** Trigger when scrolled into view instead of on mount. Default: false */\n onView?: boolean\n /** Trigger once. Default: true */\n once?: boolean\n /**\n * Reveal mode. Default: 'typing'\n * - 'typing': character-by-character like someone typing\n * - 'smooth': sliding mask reveal (cinematic feel)\n */\n variant?: \"typing\" | \"smooth\"\n /** Duration of the smooth reveal in seconds. Default: 2 */\n smoothDuration?: number\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────\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/** Flatten text prop into a single string for character counting */\nfunction flattenText(text: string | TextSegment[]): string {\n if (typeof text === \"string\") return text\n return text.map((s) => s.text).join(\"\")\n}\n\n/** Build rendered content from segments up to a character index */\nfunction buildSegmentContent(\n segments: TextSegment[],\n charIndex: number,\n): React.ReactNode[] {\n const nodes: React.ReactNode[] = []\n let remaining = charIndex\n\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i]!\n if (remaining <= 0) break\n\n const visibleChars = seg.text.slice(0, remaining)\n remaining -= visibleChars.length\n\n if (seg.className) {\n nodes.push(\n <span key={i} className={seg.className}>\n {visibleChars}\n </span>,\n )\n } else {\n nodes.push(visibleChars)\n }\n }\n\n return nodes\n}\n\n// ── Cursor ───────────────────────────────────────────────────────────\n\nfunction BlinkingCursor({ char, id }: { char: string; id: string }) {\n const name = `tw-blink-${id}`\n return (\n <>\n <span\n aria-hidden\n style={{\n animation: `${name} 0.8s step-end infinite`,\n fontWeight: \"normal\",\n }}\n >\n {char}\n </span>\n <style>{`@keyframes ${name} { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }`}</style>\n </>\n )\n}\n\n// ── Typewriter ───────────────────────────────────────────────────────\n\n/**\n * Text reveal animation with two modes:\n *\n * **typing** (default) — character-by-character like someone typing.\n * **smooth** — cinematic sliding mask reveal.\n *\n * Supports plain strings or styled segments for per-word coloring.\n * Zero wrapper divs — renders into the child element via cloneElement.\n *\n * @example\n * ```tsx\n * // Simple string\n * <Typewriter text=\"Welcome to the future.\">\n * <h1 className=\"text-4xl font-bold\" />\n * </Typewriter>\n *\n * // Per-word styling\n * <Typewriter text={[\n * { text: \"Build on \" },\n * { text: \"Intuition\", className: \"text-primary font-bold\" },\n * ]}>\n * <h1 className=\"text-4xl\" />\n * </Typewriter>\n *\n * // Smooth reveal\n * <Typewriter text=\"Cinematic reveal.\" variant=\"smooth\" smoothDuration={1.5}>\n * <h1 className=\"text-4xl font-bold\" />\n * </Typewriter>\n * ```\n */\nfunction Typewriter({\n text,\n children,\n speed = 0.04,\n delay = 0,\n cursor = true,\n cursorChar = \"|\",\n onView = false,\n once = true,\n variant = \"typing\",\n smoothDuration = 2,\n}: TypewriterProps) {\n const ref = useRef<HTMLElement>(null)\n const id = useId().replace(/:/g, \"\")\n const isInView = useInView(ref, { once, margin: \"-50px\" })\n\n const shouldAnimate = onView ? isInView : true\n\n if (variant === \"smooth\") {\n return (\n <SmoothReveal\n text={text}\n elementRef={ref}\n id={id}\n delay={delay}\n duration={smoothDuration}\n cursor={cursor}\n cursorChar={cursorChar}\n shouldAnimate={shouldAnimate}\n >\n {children}\n </SmoothReveal>\n )\n }\n\n return (\n <TypingReveal\n text={text}\n elementRef={ref}\n id={id}\n speed={speed}\n delay={delay}\n cursor={cursor}\n cursorChar={cursorChar}\n shouldAnimate={shouldAnimate}\n >\n {children}\n </TypingReveal>\n )\n}\n\n// ── Typing Reveal ────────────────────────────────────────────────────\n\nfunction TypingReveal({\n text,\n children,\n elementRef,\n id,\n speed,\n delay,\n cursor,\n cursorChar,\n shouldAnimate,\n}: {\n text: string | TextSegment[]\n children: ReactElement\n elementRef: React.RefObject<HTMLElement | null>\n id: string\n speed: number\n delay: number\n cursor: boolean\n cursorChar: string\n shouldAnimate: boolean\n}) {\n const flat = flattenText(text)\n const segments = typeof text === \"string\" ? [{ text }] : text\n const [charIndex, setCharIndex] = useState(0)\n const [started, setStarted] = useState(false)\n const [done, setDone] = useState(false)\n\n useEffect(() => {\n if (!shouldAnimate || started) return\n const timer = setTimeout(() => setStarted(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [shouldAnimate, delay, started])\n\n useEffect(() => {\n if (!started || done) return\n if (charIndex >= flat.length) {\n setDone(true)\n return\n }\n const timer = setTimeout(() => setCharIndex((prev) => prev + 1), speed * 1000)\n return () => clearTimeout(timer)\n }, [started, charIndex, flat.length, speed, done])\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 const content = started ? buildSegmentContent(segments, charIndex) : null\n const showCursor = cursor && started && !done\n\n return cloneElement(children, {\n ref: mergeRef(elementRef, existingRef),\n children: (\n <>\n {content}\n {showCursor && <BlinkingCursor char={cursorChar} id={id} />}\n </>\n ),\n } as Record<string, unknown>)\n}\n\n// ── Smooth Reveal ────────────────────────────────────────────────────\n\nfunction SmoothReveal({\n text,\n children,\n elementRef,\n id,\n delay,\n duration,\n cursor,\n cursorChar,\n shouldAnimate,\n}: {\n text: string | TextSegment[]\n children: ReactElement\n elementRef: React.RefObject<HTMLElement | null>\n id: string\n delay: number\n duration: number\n cursor: boolean\n cursorChar: string\n shouldAnimate: boolean\n}) {\n const segments = typeof text === \"string\" ? [{ text }] : text\n const [revealed, setRevealed] = useState(false)\n const [done, setDone] = useState(false)\n\n useEffect(() => {\n if (!shouldAnimate || revealed) return\n const timer = setTimeout(() => setRevealed(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [shouldAnimate, delay, revealed])\n\n useEffect(() => {\n if (!revealed) return\n const timer = setTimeout(() => setDone(true), duration * 1000)\n return () => clearTimeout(timer)\n }, [revealed, duration])\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 const existingStyle = (childProps.style ?? {}) as CSSProperties\n\n const animName = `tw-smooth-${id}`\n const showCursor = cursor && revealed && !done\n\n // Full text is always in the DOM — the mask clip animates to reveal it\n const fullContent = segments.map((seg, i) =>\n seg.className ? (\n <span key={i} className={seg.className}>\n {seg.text}\n </span>\n ) : (\n seg.text\n ),\n )\n\n return cloneElement(children, {\n ref: mergeRef(elementRef, existingRef),\n style: {\n ...existingStyle,\n whiteSpace: \"nowrap\" as const,\n overflow: \"hidden\" as const,\n ...(revealed\n ? {\n animation: `${animName} ${duration}s linear forwards`,\n }\n : {\n maxWidth: 0,\n }),\n },\n children: (\n <>\n <style>{`@keyframes ${animName} { from { max-width: 0; } to { max-width: 100%; } }`}</style>\n {fullContent}\n {showCursor && <BlinkingCursor char={cursorChar} id={id} />}\n </>\n ),\n } as Record<string, unknown>)\n}\n\n// ── Exports ──────────────────────────────────────────────────────────\n\nexport { Typewriter }\nexport type { TypewriterProps, TextSegment }\n"],"mappings":"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"}
1
+ {"version":3,"file":"typewriter.js","names":[],"sources":["../src/typewriter.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n type CSSProperties,\n type ReactElement,\n type Ref,\n cloneElement,\n isValidElement,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\"\nimport { useInView, useReducedMotion } from \"motion/react\"\n\n// ── Types ────────────────────────────────────────────────────────────\n\ninterface TextSegment {\n text: string\n /** Optional className applied to this segment (e.g., bold, colored) */\n className?: string\n}\n\ninterface TypewriterProps {\n /** Simple string or styled segments for per-word/phrase styling */\n text: string | TextSegment[]\n /** Element to render into. Receives the animated text as children. */\n children: ReactElement\n /** Time per character in seconds. Default: 0.04 */\n speed?: number\n /** Delay before typing starts in seconds. Default: 0 */\n delay?: number\n /** Show a blinking cursor during typing. Default: true */\n cursor?: boolean\n /** Cursor character. Default: '|' */\n cursorChar?: string\n /** Trigger when scrolled into view instead of on mount. Default: false */\n onView?: boolean\n /** Trigger once. Default: true */\n once?: boolean\n /**\n * Reveal mode. Default: 'typing'\n * - 'typing': character-by-character like someone typing\n * - 'smooth': sliding mask reveal (cinematic feel)\n */\n variant?: \"typing\" | \"smooth\"\n /** Duration of the smooth reveal in seconds. Default: 2 */\n smoothDuration?: number\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────\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/** Flatten text prop into a single string for character counting */\nfunction flattenText(text: string | TextSegment[]): string {\n if (typeof text === \"string\") return text\n return text.map((s) => s.text).join(\"\")\n}\n\n/** Build rendered content from segments up to a character index */\nfunction buildSegmentContent(\n segments: TextSegment[],\n charIndex: number,\n): React.ReactNode[] {\n const nodes: React.ReactNode[] = []\n let remaining = charIndex\n\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i]!\n if (remaining <= 0) break\n\n const visibleChars = seg.text.slice(0, remaining)\n remaining -= visibleChars.length\n\n if (seg.className) {\n nodes.push(\n <span key={i} className={seg.className}>\n {visibleChars}\n </span>,\n )\n } else {\n nodes.push(visibleChars)\n }\n }\n\n return nodes\n}\n\n// ── Cursor ───────────────────────────────────────────────────────────\n\nfunction BlinkingCursor({ char, id }: { char: string; id: string }) {\n const name = `tw-blink-${id}`\n return (\n <>\n <span\n aria-hidden\n style={{\n animation: `${name} 0.8s step-end infinite`,\n fontWeight: \"normal\",\n }}\n >\n {char}\n </span>\n <style>{`@keyframes ${name} { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }`}</style>\n </>\n )\n}\n\n// ── Typewriter ───────────────────────────────────────────────────────\n\n/**\n * Text reveal animation with two modes:\n *\n * **typing** (default) — character-by-character like someone typing.\n * **smooth** — cinematic sliding mask reveal.\n *\n * Supports plain strings or styled segments for per-word coloring.\n * Zero wrapper divs — renders into the child element via cloneElement.\n *\n * @example\n * ```tsx\n * // Simple string\n * <Typewriter text=\"Welcome to the future.\">\n * <h1 className=\"text-4xl font-bold\" />\n * </Typewriter>\n *\n * // Per-word styling\n * <Typewriter text={[\n * { text: \"Build on \" },\n * { text: \"Intuition\", className: \"text-primary font-bold\" },\n * ]}>\n * <h1 className=\"text-4xl\" />\n * </Typewriter>\n *\n * // Smooth reveal\n * <Typewriter text=\"Cinematic reveal.\" variant=\"smooth\" smoothDuration={1.5}>\n * <h1 className=\"text-4xl font-bold\" />\n * </Typewriter>\n * ```\n */\nfunction Typewriter({\n text,\n children,\n speed = 0.04,\n delay = 0,\n cursor = true,\n cursorChar = \"|\",\n onView = false,\n once = true,\n variant = \"typing\",\n smoothDuration = 2,\n}: TypewriterProps) {\n const ref = useRef<HTMLElement>(null)\n const id = useId().replace(/:/g, \"\")\n const isInView = useInView(ref, { once, margin: \"-50px\" })\n\n const shouldAnimate = onView ? isInView : true\n\n if (variant === \"smooth\") {\n return (\n <SmoothReveal\n text={text}\n elementRef={ref}\n id={id}\n delay={delay}\n duration={smoothDuration}\n cursor={cursor}\n cursorChar={cursorChar}\n shouldAnimate={shouldAnimate}\n >\n {children}\n </SmoothReveal>\n )\n }\n\n return (\n <TypingReveal\n text={text}\n elementRef={ref}\n id={id}\n speed={speed}\n delay={delay}\n cursor={cursor}\n cursorChar={cursorChar}\n shouldAnimate={shouldAnimate}\n >\n {children}\n </TypingReveal>\n )\n}\n\n// ── Typing Reveal ────────────────────────────────────────────────────\n\nfunction TypingReveal({\n text,\n children,\n elementRef,\n id,\n speed,\n delay,\n cursor,\n cursorChar,\n shouldAnimate,\n}: {\n text: string | TextSegment[]\n children: ReactElement\n elementRef: React.RefObject<HTMLElement | null>\n id: string\n speed: number\n delay: number\n cursor: boolean\n cursorChar: string\n shouldAnimate: boolean\n}) {\n const prefersReducedMotion = useReducedMotion()\n const flat = flattenText(text)\n const segments = typeof text === \"string\" ? [{ text }] : text\n const [charIndex, setCharIndex] = useState(0)\n const [started, setStarted] = useState(false)\n const [done, setDone] = useState(false)\n\n useEffect(() => {\n if (prefersReducedMotion || !shouldAnimate || started) return\n const timer = setTimeout(() => setStarted(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [prefersReducedMotion, shouldAnimate, delay, started])\n\n useEffect(() => {\n if (prefersReducedMotion || !started || done) return\n if (charIndex >= flat.length) {\n setDone(true)\n return\n }\n const timer = setTimeout(() => setCharIndex((prev) => prev + 1), speed * 1000)\n return () => clearTimeout(timer)\n }, [prefersReducedMotion, started, charIndex, flat.length, speed, done])\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 // Reduced motion: skip the character-by-character reveal and render the\n // full text immediately in its final resting shape (no cursor).\n const content = prefersReducedMotion\n ? buildSegmentContent(segments, flat.length)\n : started\n ? buildSegmentContent(segments, charIndex)\n : null\n const showCursor = !prefersReducedMotion && cursor && started && !done\n\n return cloneElement(children, {\n ref: mergeRef(elementRef, existingRef),\n children: (\n <>\n {content}\n {showCursor && <BlinkingCursor char={cursorChar} id={id} />}\n </>\n ),\n } as Record<string, unknown>)\n}\n\n// ── Smooth Reveal ────────────────────────────────────────────────────\n\nfunction SmoothReveal({\n text,\n children,\n elementRef,\n id,\n delay,\n duration,\n cursor,\n cursorChar,\n shouldAnimate,\n}: {\n text: string | TextSegment[]\n children: ReactElement\n elementRef: React.RefObject<HTMLElement | null>\n id: string\n delay: number\n duration: number\n cursor: boolean\n cursorChar: string\n shouldAnimate: boolean\n}) {\n const prefersReducedMotion = useReducedMotion()\n const segments = typeof text === \"string\" ? [{ text }] : text\n const [revealed, setRevealed] = useState(false)\n const [done, setDone] = useState(false)\n\n useEffect(() => {\n if (prefersReducedMotion || !shouldAnimate || revealed) return\n const timer = setTimeout(() => setRevealed(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [prefersReducedMotion, shouldAnimate, delay, revealed])\n\n useEffect(() => {\n if (prefersReducedMotion || !revealed) return\n const timer = setTimeout(() => setDone(true), duration * 1000)\n return () => clearTimeout(timer)\n }, [prefersReducedMotion, revealed, duration])\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 const existingStyle = (childProps.style ?? {}) as CSSProperties\n\n const animName = `tw-smooth-${id}`\n const showCursor = !prefersReducedMotion && cursor && revealed && !done\n\n // Full text is always in the DOM — the mask clip animates to reveal it\n const fullContent = segments.map((seg, i) =>\n seg.className ? (\n <span key={i} className={seg.className}>\n {seg.text}\n </span>\n ) : (\n seg.text\n ),\n )\n\n return cloneElement(children, {\n ref: mergeRef(elementRef, existingRef),\n style: {\n ...existingStyle,\n whiteSpace: \"nowrap\" as const,\n overflow: \"hidden\" as const,\n // Reduced motion: skip the mask animation and pin to the reveal's\n // final resting state (fully shown) with no animation or cursor.\n ...(prefersReducedMotion\n ? {\n maxWidth: \"100%\" as const,\n }\n : revealed\n ? {\n animation: `${animName} ${duration}s linear forwards`,\n }\n : {\n maxWidth: 0,\n }),\n },\n children: (\n <>\n <style>{`@keyframes ${animName} { from { max-width: 0; } to { max-width: 100%; } }`}</style>\n {fullContent}\n {showCursor && <BlinkingCursor char={cursorChar} id={id} />}\n </>\n ),\n } as Record<string, unknown>)\n}\n\n// ── Exports ──────────────────────────────────────────────────────────\n\nexport { Typewriter }\nexport type { TypewriterProps, TextSegment }\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waveso/ui",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "Wave UI component library built on Base UI and Tailwind CSS",
5
5
  "type": "module",
6
6
  "sideEffects": [