wmx-motion 0.2.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.
@@ -0,0 +1,11 @@
1
+ import type { HTMLAttributes } from "react";
2
+ export interface CountUpProps extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
3
+ end: number;
4
+ start?: number;
5
+ duration?: number;
6
+ decimals?: number;
7
+ prefix?: string;
8
+ suffix?: string;
9
+ once?: boolean;
10
+ }
11
+ export declare function CountUp({ end, start, duration, decimals, prefix, suffix, once, className, ...rest }: CountUpProps): import("react").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { useInView } from "../useInView.js";
4
+ export function CountUp({ end, start = 0, duration = 1200, decimals = 0, prefix = "", suffix = "", once = true, className, ...rest }) {
5
+ const [ref, inView] = useInView({ threshold: 0.3, once });
6
+ const [value, setValue] = useState(start);
7
+ const frameRef = useRef(0);
8
+ useEffect(() => {
9
+ if (!inView)
10
+ return;
11
+ const startTime = performance.now();
12
+ const tick = (now) => {
13
+ const progress = Math.min((now - startTime) / duration, 1);
14
+ const eased = 1 - Math.pow(1 - progress, 3);
15
+ setValue(start + (end - start) * eased);
16
+ if (progress < 1) {
17
+ frameRef.current = requestAnimationFrame(tick);
18
+ }
19
+ };
20
+ frameRef.current = requestAnimationFrame(tick);
21
+ return () => cancelAnimationFrame(frameRef.current);
22
+ }, [inView, start, end, duration]);
23
+ return (_jsxs("span", { ref: ref, className: ["wmx-countup", className].filter(Boolean).join(" "), ...rest, children: [prefix, value.toFixed(decimals), suffix] }));
24
+ }
@@ -0,0 +1,44 @@
1
+ .wmx-reveal {
2
+ opacity: 0;
3
+ transition: opacity var(--wmx-reveal-duration, 600ms) ease,
4
+ transform var(--wmx-reveal-duration, 600ms) cubic-bezier(0.16, 1, 0.3, 1);
5
+ transition-delay: var(--wmx-reveal-delay, 0ms);
6
+ will-change: opacity, transform;
7
+ }
8
+
9
+ .wmx-reveal--up {
10
+ transform: translateY(var(--wmx-reveal-distance, 24px));
11
+ }
12
+
13
+ .wmx-reveal--down {
14
+ transform: translateY(calc(-1 * var(--wmx-reveal-distance, 24px)));
15
+ }
16
+
17
+ .wmx-reveal--left {
18
+ transform: translateX(var(--wmx-reveal-distance, 24px));
19
+ }
20
+
21
+ .wmx-reveal--right {
22
+ transform: translateX(calc(-1 * var(--wmx-reveal-distance, 24px)));
23
+ }
24
+
25
+ .wmx-reveal--zoom {
26
+ transform: scale(0.94);
27
+ }
28
+
29
+ .wmx-reveal--fade {
30
+ transform: none;
31
+ }
32
+
33
+ .wmx-reveal--visible {
34
+ opacity: 1;
35
+ transform: none;
36
+ }
37
+
38
+ @media (prefers-reduced-motion: reduce) {
39
+ .wmx-reveal {
40
+ transition: none;
41
+ transform: none;
42
+ opacity: 1;
43
+ }
44
+ }
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import "./Reveal.css";
3
+ export type RevealDirection = "up" | "down" | "left" | "right" | "fade" | "zoom";
4
+ export interface RevealProps extends HTMLAttributes<HTMLDivElement> {
5
+ direction?: RevealDirection;
6
+ delay?: number;
7
+ duration?: number;
8
+ distance?: number;
9
+ once?: boolean;
10
+ threshold?: number;
11
+ children: ReactNode;
12
+ }
13
+ export declare function Reveal({ direction, delay, duration, distance, once, threshold, className, style, children, ...rest }: RevealProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useInView } from "../useInView.js";
3
+ import "./Reveal.css";
4
+ export function Reveal({ direction = "up", delay = 0, duration = 600, distance = 24, once = true, threshold = 0.15, className, style, children, ...rest }) {
5
+ const [ref, inView] = useInView({ threshold, once });
6
+ const classes = [
7
+ "wmx-reveal",
8
+ `wmx-reveal--${direction}`,
9
+ inView && "wmx-reveal--visible",
10
+ className,
11
+ ]
12
+ .filter(Boolean)
13
+ .join(" ");
14
+ const revealStyle = {
15
+ ...style,
16
+ ["--wmx-reveal-duration"]: `${duration}ms`,
17
+ ["--wmx-reveal-delay"]: `${delay}ms`,
18
+ ["--wmx-reveal-distance"]: `${distance}px`,
19
+ };
20
+ return (_jsx("div", { ref: ref, className: classes, style: revealStyle, ...rest, children: children }));
21
+ }
@@ -0,0 +1,18 @@
1
+ .wmx-stagger__item {
2
+ opacity: 0;
3
+ transform: translateY(16px);
4
+ transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
5
+ }
6
+
7
+ .wmx-stagger--visible .wmx-stagger__item {
8
+ opacity: 1;
9
+ transform: none;
10
+ }
11
+
12
+ @media (prefers-reduced-motion: reduce) {
13
+ .wmx-stagger__item {
14
+ transition: none;
15
+ opacity: 1;
16
+ transform: none;
17
+ }
18
+ }
@@ -0,0 +1,9 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import "./Stagger.css";
3
+ export interface StaggerProps extends HTMLAttributes<HTMLDivElement> {
4
+ gap?: number;
5
+ once?: boolean;
6
+ threshold?: number;
7
+ children: ReactNode;
8
+ }
9
+ export declare function Stagger({ gap, once, threshold, className, children, ...rest }: StaggerProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Children, cloneElement, isValidElement } from "react";
3
+ import { useInView } from "../useInView.js";
4
+ import "./Stagger.css";
5
+ export function Stagger({ gap = 80, once = true, threshold = 0.15, className, children, ...rest }) {
6
+ const [ref, inView] = useInView({ threshold, once });
7
+ const classes = ["wmx-stagger", inView && "wmx-stagger--visible", className]
8
+ .filter(Boolean)
9
+ .join(" ");
10
+ return (_jsx("div", { ref: ref, className: classes, ...rest, children: Children.map(children, (child, index) => {
11
+ if (!isValidElement(child))
12
+ return child;
13
+ return cloneElement(child, {
14
+ className: ["wmx-stagger__item", child.props.className].filter(Boolean).join(" "),
15
+ style: {
16
+ ...child.props.style,
17
+ transitionDelay: inView ? `${index * gap}ms` : undefined,
18
+ },
19
+ });
20
+ }) }));
21
+ }
@@ -0,0 +1,45 @@
1
+ .wmx-transition {
2
+ transition-property: opacity, transform;
3
+ transition-duration: var(--wmx-transition-duration, 200ms);
4
+ transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
5
+ }
6
+
7
+ .wmx-transition--fade {
8
+ opacity: 0;
9
+ }
10
+ .wmx-transition--fade.wmx-transition--entered {
11
+ opacity: 1;
12
+ }
13
+
14
+ .wmx-transition--scale {
15
+ opacity: 0;
16
+ transform: scale(0.95);
17
+ }
18
+ .wmx-transition--scale.wmx-transition--entered {
19
+ opacity: 1;
20
+ transform: scale(1);
21
+ }
22
+
23
+ .wmx-transition--slide-up {
24
+ opacity: 0;
25
+ transform: translateY(12px);
26
+ }
27
+ .wmx-transition--slide-up.wmx-transition--entered {
28
+ opacity: 1;
29
+ transform: translateY(0);
30
+ }
31
+
32
+ .wmx-transition--slide-down {
33
+ opacity: 0;
34
+ transform: translateY(-12px);
35
+ }
36
+ .wmx-transition--slide-down.wmx-transition--entered {
37
+ opacity: 1;
38
+ transform: translateY(0);
39
+ }
40
+
41
+ @media (prefers-reduced-motion: reduce) {
42
+ .wmx-transition {
43
+ transition: none;
44
+ }
45
+ }
@@ -0,0 +1,10 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import "./Transition.css";
3
+ export type TransitionType = "fade" | "scale" | "slide-up" | "slide-down";
4
+ export interface TransitionProps extends HTMLAttributes<HTMLDivElement> {
5
+ show: boolean;
6
+ type?: TransitionType;
7
+ duration?: number;
8
+ children: ReactNode;
9
+ }
10
+ export declare function Transition({ show, type, duration, className, style, children, ...rest }: TransitionProps): import("react").JSX.Element | null;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import "./Transition.css";
4
+ export function Transition({ show, type = "fade", duration = 200, className, style, children, ...rest }) {
5
+ const [mounted, setMounted] = useState(show);
6
+ const [entered, setEntered] = useState(show);
7
+ const timeoutRef = useRef(undefined);
8
+ useEffect(() => {
9
+ if (timeoutRef.current)
10
+ clearTimeout(timeoutRef.current);
11
+ if (show) {
12
+ setMounted(true);
13
+ const raf = requestAnimationFrame(() => setEntered(true));
14
+ return () => cancelAnimationFrame(raf);
15
+ }
16
+ setEntered(false);
17
+ timeoutRef.current = setTimeout(() => setMounted(false), duration);
18
+ return () => {
19
+ if (timeoutRef.current)
20
+ clearTimeout(timeoutRef.current);
21
+ };
22
+ }, [show, duration]);
23
+ if (!mounted)
24
+ return null;
25
+ const classes = [
26
+ "wmx-transition",
27
+ `wmx-transition--${type}`,
28
+ entered && "wmx-transition--entered",
29
+ className,
30
+ ]
31
+ .filter(Boolean)
32
+ .join(" ");
33
+ const transitionStyle = {
34
+ ...style,
35
+ ["--wmx-transition-duration"]: `${duration}ms`,
36
+ };
37
+ return (_jsx("div", { className: classes, style: transitionStyle, ...rest, children: children }));
38
+ }
@@ -0,0 +1,11 @@
1
+ export { useInView } from "./useInView.js";
2
+ export type { UseInViewOptions } from "./useInView.js";
3
+ export { useReducedMotion } from "./useReducedMotion.js";
4
+ export { Reveal } from "./Reveal/Reveal.js";
5
+ export type { RevealProps, RevealDirection } from "./Reveal/Reveal.js";
6
+ export { Stagger } from "./Stagger/Stagger.js";
7
+ export type { StaggerProps } from "./Stagger/Stagger.js";
8
+ export { Transition } from "./Transition/Transition.js";
9
+ export type { TransitionProps, TransitionType } from "./Transition/Transition.js";
10
+ export { CountUp } from "./CountUp/CountUp.js";
11
+ export type { CountUpProps } from "./CountUp/CountUp.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { useInView } from "./useInView.js";
2
+ export { useReducedMotion } from "./useReducedMotion.js";
3
+ export { Reveal } from "./Reveal/Reveal.js";
4
+ export { Stagger } from "./Stagger/Stagger.js";
5
+ export { Transition } from "./Transition/Transition.js";
6
+ export { CountUp } from "./CountUp/CountUp.js";
@@ -0,0 +1,6 @@
1
+ export interface UseInViewOptions {
2
+ threshold?: number | number[];
3
+ rootMargin?: string;
4
+ once?: boolean;
5
+ }
6
+ export declare function useInView<T extends Element = HTMLDivElement>({ threshold, rootMargin, once, }?: UseInViewOptions): readonly [import("react").RefObject<T>, boolean];
@@ -0,0 +1,27 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ export function useInView({ threshold = 0.15, rootMargin = "0px", once = true, } = {}) {
3
+ const ref = useRef(null);
4
+ const [inView, setInView] = useState(false);
5
+ useEffect(() => {
6
+ const node = ref.current;
7
+ if (!node)
8
+ return;
9
+ if (typeof IntersectionObserver === "undefined") {
10
+ setInView(true);
11
+ return;
12
+ }
13
+ const observer = new IntersectionObserver(([entry]) => {
14
+ if (entry.isIntersecting) {
15
+ setInView(true);
16
+ if (once)
17
+ observer.disconnect();
18
+ }
19
+ else if (!once) {
20
+ setInView(false);
21
+ }
22
+ }, { threshold, rootMargin });
23
+ observer.observe(node);
24
+ return () => observer.disconnect();
25
+ }, [threshold, rootMargin, once]);
26
+ return [ref, inView];
27
+ }
@@ -0,0 +1 @@
1
+ export declare function useReducedMotion(): boolean;
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from "react";
2
+ const QUERY = "(prefers-reduced-motion: reduce)";
3
+ export function useReducedMotion() {
4
+ const [reduced, setReduced] = useState(() => typeof window !== "undefined" ? window.matchMedia(QUERY).matches : false);
5
+ useEffect(() => {
6
+ const mql = window.matchMedia(QUERY);
7
+ const handler = () => setReduced(mql.matches);
8
+ mql.addEventListener("change", handler);
9
+ return () => mql.removeEventListener("change", handler);
10
+ }, []);
11
+ return reduced;
12
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "wmx-motion",
3
+ "version": "0.2.5",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc --project tsconfig.json && node scripts/copy-css.mjs",
9
+ "dev": "tsc --project tsconfig.json --watch"
10
+ },
11
+ "peerDependencies": {
12
+ "react": "^18.2.0"
13
+ },
14
+ "devDependencies": {
15
+ "@types/react": "^18.2.0",
16
+ "typescript": "^5.4.0"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "package.json"
22
+ ]
23
+ }
@@ -0,0 +1,55 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { HTMLAttributes } from "react";
3
+ import { useInView } from "../useInView.js";
4
+
5
+ export interface CountUpProps extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
6
+ end: number;
7
+ start?: number;
8
+ duration?: number;
9
+ decimals?: number;
10
+ prefix?: string;
11
+ suffix?: string;
12
+ once?: boolean;
13
+ }
14
+
15
+ export function CountUp({
16
+ end,
17
+ start = 0,
18
+ duration = 1200,
19
+ decimals = 0,
20
+ prefix = "",
21
+ suffix = "",
22
+ once = true,
23
+ className,
24
+ ...rest
25
+ }: CountUpProps) {
26
+ const [ref, inView] = useInView<HTMLSpanElement>({ threshold: 0.3, once });
27
+ const [value, setValue] = useState(start);
28
+ const frameRef = useRef(0);
29
+
30
+ useEffect(() => {
31
+ if (!inView) return;
32
+
33
+ const startTime = performance.now();
34
+
35
+ const tick = (now: number) => {
36
+ const progress = Math.min((now - startTime) / duration, 1);
37
+ const eased = 1 - Math.pow(1 - progress, 3);
38
+ setValue(start + (end - start) * eased);
39
+ if (progress < 1) {
40
+ frameRef.current = requestAnimationFrame(tick);
41
+ }
42
+ };
43
+
44
+ frameRef.current = requestAnimationFrame(tick);
45
+ return () => cancelAnimationFrame(frameRef.current);
46
+ }, [inView, start, end, duration]);
47
+
48
+ return (
49
+ <span ref={ref} className={["wmx-countup", className].filter(Boolean).join(" ")} {...rest}>
50
+ {prefix}
51
+ {value.toFixed(decimals)}
52
+ {suffix}
53
+ </span>
54
+ );
55
+ }
@@ -0,0 +1,44 @@
1
+ .wmx-reveal {
2
+ opacity: 0;
3
+ transition: opacity var(--wmx-reveal-duration, 600ms) ease,
4
+ transform var(--wmx-reveal-duration, 600ms) cubic-bezier(0.16, 1, 0.3, 1);
5
+ transition-delay: var(--wmx-reveal-delay, 0ms);
6
+ will-change: opacity, transform;
7
+ }
8
+
9
+ .wmx-reveal--up {
10
+ transform: translateY(var(--wmx-reveal-distance, 24px));
11
+ }
12
+
13
+ .wmx-reveal--down {
14
+ transform: translateY(calc(-1 * var(--wmx-reveal-distance, 24px)));
15
+ }
16
+
17
+ .wmx-reveal--left {
18
+ transform: translateX(var(--wmx-reveal-distance, 24px));
19
+ }
20
+
21
+ .wmx-reveal--right {
22
+ transform: translateX(calc(-1 * var(--wmx-reveal-distance, 24px)));
23
+ }
24
+
25
+ .wmx-reveal--zoom {
26
+ transform: scale(0.94);
27
+ }
28
+
29
+ .wmx-reveal--fade {
30
+ transform: none;
31
+ }
32
+
33
+ .wmx-reveal--visible {
34
+ opacity: 1;
35
+ transform: none;
36
+ }
37
+
38
+ @media (prefers-reduced-motion: reduce) {
39
+ .wmx-reveal {
40
+ transition: none;
41
+ transform: none;
42
+ opacity: 1;
43
+ }
44
+ }
@@ -0,0 +1,52 @@
1
+ import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
2
+ import { useInView } from "../useInView.js";
3
+ import "./Reveal.css";
4
+
5
+ export type RevealDirection = "up" | "down" | "left" | "right" | "fade" | "zoom";
6
+
7
+ export interface RevealProps extends HTMLAttributes<HTMLDivElement> {
8
+ direction?: RevealDirection;
9
+ delay?: number;
10
+ duration?: number;
11
+ distance?: number;
12
+ once?: boolean;
13
+ threshold?: number;
14
+ children: ReactNode;
15
+ }
16
+
17
+ export function Reveal({
18
+ direction = "up",
19
+ delay = 0,
20
+ duration = 600,
21
+ distance = 24,
22
+ once = true,
23
+ threshold = 0.15,
24
+ className,
25
+ style,
26
+ children,
27
+ ...rest
28
+ }: RevealProps) {
29
+ const [ref, inView] = useInView<HTMLDivElement>({ threshold, once });
30
+
31
+ const classes = [
32
+ "wmx-reveal",
33
+ `wmx-reveal--${direction}`,
34
+ inView && "wmx-reveal--visible",
35
+ className,
36
+ ]
37
+ .filter(Boolean)
38
+ .join(" ");
39
+
40
+ const revealStyle: CSSProperties = {
41
+ ...style,
42
+ ["--wmx-reveal-duration" as string]: `${duration}ms`,
43
+ ["--wmx-reveal-delay" as string]: `${delay}ms`,
44
+ ["--wmx-reveal-distance" as string]: `${distance}px`,
45
+ };
46
+
47
+ return (
48
+ <div ref={ref} className={classes} style={revealStyle} {...rest}>
49
+ {children}
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,18 @@
1
+ .wmx-stagger__item {
2
+ opacity: 0;
3
+ transform: translateY(16px);
4
+ transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
5
+ }
6
+
7
+ .wmx-stagger--visible .wmx-stagger__item {
8
+ opacity: 1;
9
+ transform: none;
10
+ }
11
+
12
+ @media (prefers-reduced-motion: reduce) {
13
+ .wmx-stagger__item {
14
+ transition: none;
15
+ opacity: 1;
16
+ transform: none;
17
+ }
18
+ }
@@ -0,0 +1,46 @@
1
+ import { Children, cloneElement, isValidElement } from "react";
2
+ import type { CSSProperties, HTMLAttributes, ReactElement, ReactNode } from "react";
3
+ import { useInView } from "../useInView.js";
4
+ import "./Stagger.css";
5
+
6
+ interface StaggerChildProps {
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ }
10
+
11
+ export interface StaggerProps extends HTMLAttributes<HTMLDivElement> {
12
+ gap?: number;
13
+ once?: boolean;
14
+ threshold?: number;
15
+ children: ReactNode;
16
+ }
17
+
18
+ export function Stagger({
19
+ gap = 80,
20
+ once = true,
21
+ threshold = 0.15,
22
+ className,
23
+ children,
24
+ ...rest
25
+ }: StaggerProps) {
26
+ const [ref, inView] = useInView<HTMLDivElement>({ threshold, once });
27
+ const classes = ["wmx-stagger", inView && "wmx-stagger--visible", className]
28
+ .filter(Boolean)
29
+ .join(" ");
30
+
31
+ return (
32
+ <div ref={ref} className={classes} {...rest}>
33
+ {Children.map(children, (child, index) => {
34
+ if (!isValidElement<StaggerChildProps>(child)) return child;
35
+
36
+ return cloneElement(child as ReactElement<StaggerChildProps>, {
37
+ className: ["wmx-stagger__item", child.props.className].filter(Boolean).join(" "),
38
+ style: {
39
+ ...child.props.style,
40
+ transitionDelay: inView ? `${index * gap}ms` : undefined,
41
+ },
42
+ });
43
+ })}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,45 @@
1
+ .wmx-transition {
2
+ transition-property: opacity, transform;
3
+ transition-duration: var(--wmx-transition-duration, 200ms);
4
+ transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
5
+ }
6
+
7
+ .wmx-transition--fade {
8
+ opacity: 0;
9
+ }
10
+ .wmx-transition--fade.wmx-transition--entered {
11
+ opacity: 1;
12
+ }
13
+
14
+ .wmx-transition--scale {
15
+ opacity: 0;
16
+ transform: scale(0.95);
17
+ }
18
+ .wmx-transition--scale.wmx-transition--entered {
19
+ opacity: 1;
20
+ transform: scale(1);
21
+ }
22
+
23
+ .wmx-transition--slide-up {
24
+ opacity: 0;
25
+ transform: translateY(12px);
26
+ }
27
+ .wmx-transition--slide-up.wmx-transition--entered {
28
+ opacity: 1;
29
+ transform: translateY(0);
30
+ }
31
+
32
+ .wmx-transition--slide-down {
33
+ opacity: 0;
34
+ transform: translateY(-12px);
35
+ }
36
+ .wmx-transition--slide-down.wmx-transition--entered {
37
+ opacity: 1;
38
+ transform: translateY(0);
39
+ }
40
+
41
+ @media (prefers-reduced-motion: reduce) {
42
+ .wmx-transition {
43
+ transition: none;
44
+ }
45
+ }
@@ -0,0 +1,64 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
3
+ import "./Transition.css";
4
+
5
+ export type TransitionType = "fade" | "scale" | "slide-up" | "slide-down";
6
+
7
+ export interface TransitionProps extends HTMLAttributes<HTMLDivElement> {
8
+ show: boolean;
9
+ type?: TransitionType;
10
+ duration?: number;
11
+ children: ReactNode;
12
+ }
13
+
14
+ export function Transition({
15
+ show,
16
+ type = "fade",
17
+ duration = 200,
18
+ className,
19
+ style,
20
+ children,
21
+ ...rest
22
+ }: TransitionProps) {
23
+ const [mounted, setMounted] = useState(show);
24
+ const [entered, setEntered] = useState(show);
25
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
26
+
27
+ useEffect(() => {
28
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
29
+
30
+ if (show) {
31
+ setMounted(true);
32
+ const raf = requestAnimationFrame(() => setEntered(true));
33
+ return () => cancelAnimationFrame(raf);
34
+ }
35
+
36
+ setEntered(false);
37
+ timeoutRef.current = setTimeout(() => setMounted(false), duration);
38
+ return () => {
39
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
40
+ };
41
+ }, [show, duration]);
42
+
43
+ if (!mounted) return null;
44
+
45
+ const classes = [
46
+ "wmx-transition",
47
+ `wmx-transition--${type}`,
48
+ entered && "wmx-transition--entered",
49
+ className,
50
+ ]
51
+ .filter(Boolean)
52
+ .join(" ");
53
+
54
+ const transitionStyle: CSSProperties = {
55
+ ...style,
56
+ ["--wmx-transition-duration" as string]: `${duration}ms`,
57
+ };
58
+
59
+ return (
60
+ <div className={classes} style={transitionStyle} {...rest}>
61
+ {children}
62
+ </div>
63
+ );
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { useInView } from "./useInView.js";
2
+ export type { UseInViewOptions } from "./useInView.js";
3
+
4
+ export { useReducedMotion } from "./useReducedMotion.js";
5
+
6
+ export { Reveal } from "./Reveal/Reveal.js";
7
+ export type { RevealProps, RevealDirection } from "./Reveal/Reveal.js";
8
+
9
+ export { Stagger } from "./Stagger/Stagger.js";
10
+ export type { StaggerProps } from "./Stagger/Stagger.js";
11
+
12
+ export { Transition } from "./Transition/Transition.js";
13
+ export type { TransitionProps, TransitionType } from "./Transition/Transition.js";
14
+
15
+ export { CountUp } from "./CountUp/CountUp.js";
16
+ export type { CountUpProps } from "./CountUp/CountUp.js";
@@ -0,0 +1,43 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ export interface UseInViewOptions {
4
+ threshold?: number | number[];
5
+ rootMargin?: string;
6
+ once?: boolean;
7
+ }
8
+
9
+ export function useInView<T extends Element = HTMLDivElement>({
10
+ threshold = 0.15,
11
+ rootMargin = "0px",
12
+ once = true,
13
+ }: UseInViewOptions = {}) {
14
+ const ref = useRef<T>(null);
15
+ const [inView, setInView] = useState(false);
16
+
17
+ useEffect(() => {
18
+ const node = ref.current;
19
+ if (!node) return;
20
+
21
+ if (typeof IntersectionObserver === "undefined") {
22
+ setInView(true);
23
+ return;
24
+ }
25
+
26
+ const observer = new IntersectionObserver(
27
+ ([entry]) => {
28
+ if (entry.isIntersecting) {
29
+ setInView(true);
30
+ if (once) observer.disconnect();
31
+ } else if (!once) {
32
+ setInView(false);
33
+ }
34
+ },
35
+ { threshold, rootMargin }
36
+ );
37
+
38
+ observer.observe(node);
39
+ return () => observer.disconnect();
40
+ }, [threshold, rootMargin, once]);
41
+
42
+ return [ref, inView] as const;
43
+ }
@@ -0,0 +1,18 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ const QUERY = "(prefers-reduced-motion: reduce)";
4
+
5
+ export function useReducedMotion(): boolean {
6
+ const [reduced, setReduced] = useState(() =>
7
+ typeof window !== "undefined" ? window.matchMedia(QUERY).matches : false
8
+ );
9
+
10
+ useEffect(() => {
11
+ const mql = window.matchMedia(QUERY);
12
+ const handler = () => setReduced(mql.matches);
13
+ mql.addEventListener("change", handler);
14
+ return () => mql.removeEventListener("change", handler);
15
+ }, []);
16
+
17
+ return reduced;
18
+ }