wavelet init

This commit is contained in:
ryan
2026-06-18 15:24:48 +08:00
parent d6a7011885
commit 99738bbc17
714 changed files with 139987 additions and 0 deletions
@@ -0,0 +1,57 @@
'use client';
import * as React from 'react';
import {cva, type VariantProps} from 'class-variance-authority';
import {
Button as ButtonPrimitive,
type ButtonProps as ButtonPrimitiveProps,
} from '@/components/animate-ui/primitives/buttons/button';
import {cn} from '@/lib/utils';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[box-shadow,_color,_background-color,_border-color,_outline-color,_text-decoration-color,_fill,_stroke] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
accent: 'bg-accent text-accent-foreground shadow-xs hover:bg-accent/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8 rounded-md',
'icon-lg': 'size-10 rounded-md',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
type ButtonProps = ButtonPrimitiveProps & VariantProps<typeof buttonVariants>;
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<ButtonPrimitive
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants, type ButtonProps };
@@ -0,0 +1,56 @@
'use client';
import * as React from 'react';
import {type VariantProps} from 'class-variance-authority';
import {buttonVariants} from '@/components/animate-ui/components/buttons/button';
import {
RippleButton as RippleButtonPrimitive,
type RippleButtonProps as RippleButtonPrimitiveProps,
RippleButtonRipples as RippleButtonRipplesPrimitive,
type RippleButtonRipplesProps as RippleButtonRipplesPrimitiveProps,
} from '@/components/animate-ui/primitives/buttons/ripple';
import {cn} from '@/lib/utils';
const rippleButtonVariants = {
default: '[--ripple-button-ripple-color:var(--primary-foreground)]',
accent: '[--ripple-button-ripple-color:var(--accent-foreground)]',
destructive: '[--ripple-button-ripple-color:var(--destructive-foreground)]',
outline: '[--ripple-button-ripple-color:var(--foreground)]',
secondary: '[--ripple-button-ripple-color:var(--secondary-foreground)]',
ghost: '[--ripple-button-ripple-color:var(--foreground)]',
link: '[--ripple-button-ripple-color:var(--primary-foreground)]',
};
type RippleButtonProps = RippleButtonPrimitiveProps &
VariantProps<typeof buttonVariants>;
function RippleButton({
className,
variant,
size,
...props
}: RippleButtonProps) {
return (
<RippleButtonPrimitive
className={cn(
buttonVariants({ variant, size, className }),
rippleButtonVariants[variant as keyof typeof rippleButtonVariants],
)}
{...props}
/>
);
}
type RippleButtonRipplesProps = RippleButtonRipplesPrimitiveProps;
function RippleButtonRipples(props: RippleButtonRipplesProps) {
return <RippleButtonRipplesPrimitive {...props} />;
}
export {
RippleButton,
RippleButtonRipples,
type RippleButtonProps,
type RippleButtonRipplesProps,
};
@@ -0,0 +1,70 @@
'use client';
import * as React from 'react';
import {motion, type Variants} from 'motion/react';
import {getVariants, type IconProps, IconWrapper, useAnimateIconContext,} from '@/components/animate-ui/icons/icon';
type CheckProps = IconProps<keyof typeof animations>;
const animations = {
default: {
path: {
initial: {
pathLength: 1,
opacity: 1,
scale: 1,
},
animate: {
pathLength: [0, 1],
opacity: [0, 1],
scale: [1, 1.1, 1],
transition: {
duration: 0.6,
ease: 'easeInOut',
},
},
},
} satisfies Record<string, Variants>,
} as const;
function IconComponent({ size, ...props }: CheckProps) {
const { controls } = useAnimateIconContext();
const variants = getVariants(animations);
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
initial="initial"
animate={controls}
{...props}
>
<motion.path
d="m4 12 5 5L20 6"
variants={variants.path}
initial="initial"
animate={controls}
/>
</motion.svg>
);
}
function Check(props: CheckProps) {
return <IconWrapper icon={IconComponent} {...props} />;
}
export {
animations,
Check,
Check as CheckIcon,
type CheckProps,
type CheckProps as CheckIconProps,
};
@@ -0,0 +1,73 @@
'use client';
import * as React from 'react';
import {motion, type Variants} from 'motion/react';
import {getVariants, type IconProps, IconWrapper, useAnimateIconContext,} from '@/components/animate-ui/icons/icon';
type ChevronDownProps = IconProps<keyof typeof animations>;
const animations = {
default: {
path: {
initial: {
y: 0,
transition: { duration: 0.3, ease: 'easeInOut' },
},
animate: {
y: 4,
transition: { duration: 0.3, ease: 'easeInOut' },
},
},
} satisfies Record<string, Variants>,
'default-loop': {
path: {
initial: {
y: 0,
},
animate: {
y: [0, 4, 0],
transition: { duration: 0.6, ease: 'easeInOut' },
},
},
} satisfies Record<string, Variants>,
} as const;
function IconComponent({ size, ...props }: ChevronDownProps) {
const { controls } = useAnimateIconContext();
const variants = getVariants(animations);
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<motion.path
d="m6 9 6 6 6-6"
variants={variants.path}
initial="initial"
animate={controls}
/>
</motion.svg>
);
}
function ChevronDown(props: ChevronDownProps) {
return <IconWrapper icon={IconComponent} {...props} />;
}
export {
animations,
ChevronDown,
ChevronDown as ChevronDownIcon,
type ChevronDownProps,
type ChevronDownProps as ChevronDownIconProps,
};
@@ -0,0 +1,73 @@
'use client';
import * as React from 'react';
import {motion, type Variants} from 'motion/react';
import {getVariants, type IconProps, IconWrapper, useAnimateIconContext,} from '@/components/animate-ui/icons/icon';
type ChevronLeftProps = IconProps<keyof typeof animations>;
const animations = {
default: {
path: {
initial: {
x: 0,
transition: { duration: 0.3, ease: 'easeInOut' },
},
animate: {
x: -4,
transition: { duration: 0.3, ease: 'easeInOut' },
},
},
} satisfies Record<string, Variants>,
'default-loop': {
path: {
initial: {
x: 0,
},
animate: {
x: [0, -4, 0],
transition: { duration: 0.6, ease: 'easeInOut' },
},
},
} satisfies Record<string, Variants>,
} as const;
function IconComponent({ size, ...props }: ChevronLeftProps) {
const { controls } = useAnimateIconContext();
const variants = getVariants(animations);
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<motion.path
d="m15 18-6-6 6-6"
variants={variants.path}
initial="initial"
animate={controls}
/>
</motion.svg>
);
}
function ChevronLeft(props: ChevronLeftProps) {
return <IconWrapper icon={IconComponent} {...props} />;
}
export {
animations,
ChevronLeft,
ChevronLeft as ChevronLeftIcon,
type ChevronLeftProps,
type ChevronLeftProps as ChevronLeftIconProps,
};
@@ -0,0 +1,73 @@
'use client';
import * as React from 'react';
import {motion, type Variants} from 'motion/react';
import {getVariants, type IconProps, IconWrapper, useAnimateIconContext,} from '@/components/animate-ui/icons/icon';
type ChevronRightProps = IconProps<keyof typeof animations>;
const animations = {
default: {
path: {
initial: {
x: 0,
transition: { duration: 0.3, ease: 'easeInOut' },
},
animate: {
x: 4,
transition: { duration: 0.3, ease: 'easeInOut' },
},
},
} satisfies Record<string, Variants>,
'default-loop': {
path: {
initial: {
x: 0,
},
animate: {
x: [0, 4, 0],
transition: { duration: 0.6, ease: 'easeInOut' },
},
},
} satisfies Record<string, Variants>,
} as const;
function IconComponent({ size, ...props }: ChevronRightProps) {
const { controls } = useAnimateIconContext();
const variants = getVariants(animations);
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<motion.path
d="m9 18 6-6-6-6"
variants={variants.path}
initial="initial"
animate={controls}
/>
</motion.svg>
);
}
function ChevronRight(props: ChevronRightProps) {
return <IconWrapper icon={IconComponent} {...props} />;
}
export {
animations,
ChevronRight,
ChevronRight as ChevronRightIcon,
type ChevronRightProps,
type ChevronRightProps as ChevronRightIconProps,
};
@@ -0,0 +1,77 @@
'use client';
import * as React from 'react';
import {motion, type Variants} from 'motion/react';
import {getVariants, type IconProps, IconWrapper, useAnimateIconContext,} from '@/components/animate-ui/icons/icon';
type CircleCheckProps = IconProps<keyof typeof animations>;
const animations = {
default: {
circle: {},
path: {
initial: {
pathLength: 1,
opacity: 1,
scale: 1,
},
animate: {
pathLength: [0, 1],
opacity: [0, 1],
scale: [1, 1.1, 1],
transition: {
duration: 0.6,
ease: 'easeInOut',
},
},
},
} satisfies Record<string, Variants>,
} as const;
function IconComponent({ size, ...props }: CircleCheckProps) {
const { controls } = useAnimateIconContext();
const variants = getVariants(animations);
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<motion.circle
cx="12"
cy="12"
r="10"
variants={variants.circle}
initial="initial"
animate={controls}
/>
<motion.path
d="m9 12 2 2 4-4"
variants={variants.path}
initial="initial"
animate={controls}
/>
</motion.svg>
);
}
function CircleCheck(props: CircleCheckProps) {
return <IconWrapper icon={IconComponent} {...props} />;
}
export {
animations,
CircleCheck,
CircleCheck as CircleCheckIcon,
type CircleCheckProps,
type CircleCheckProps as CircleCheckIconProps,
};
@@ -0,0 +1,655 @@
'use client';
import * as React from 'react';
import {
type HTMLMotionProps,
type LegacyAnimationControls,
motion,
type SVGMotionProps,
useAnimation,
type UseInViewOptions,
type Variants,
} from 'motion/react';
import {cn} from '@/lib/utils';
import {useIsInView} from '@/hooks/use-is-in-view';
import {Slot, type WithAsChild} from '@/components/animate-ui/primitives/animate/slot';
const staticAnimations = {
path: {
initial: { pathLength: 1 },
animate: {
pathLength: [0.05, 1],
transition: {
duration: 0.8,
ease: 'easeInOut',
},
},
} as Variants,
'path-loop': {
initial: { pathLength: 1 },
animate: {
pathLength: [1, 0.05, 1],
transition: {
duration: 1.6,
ease: 'easeInOut',
},
},
} as Variants,
} as const;
type StaticAnimations = keyof typeof staticAnimations;
type TriggerProp<T = string> = boolean | StaticAnimations | T;
type Trigger = TriggerProp<string>;
type AnimateIconContextValue = {
controls: LegacyAnimationControls | undefined;
animation: StaticAnimations | string;
loop: boolean;
loopDelay: number;
active: boolean;
animate?: Trigger;
initialOnAnimateEnd?: boolean;
completeOnStop?: boolean;
persistOnAnimateEnd?: boolean;
delay?: number;
};
type DefaultIconProps<T = string> = {
animate?: TriggerProp<T>;
animateOnHover?: TriggerProp<T>;
animateOnTap?: TriggerProp<T>;
animateOnView?: TriggerProp<T>;
animateOnViewMargin?: UseInViewOptions['margin'];
animateOnViewOnce?: boolean;
animation?: T | StaticAnimations;
loop?: boolean;
loopDelay?: number;
initialOnAnimateEnd?: boolean;
completeOnStop?: boolean;
persistOnAnimateEnd?: boolean;
delay?: number;
};
type AnimateIconProps<T = string> = WithAsChild<
HTMLMotionProps<'span'> &
DefaultIconProps<T> & {
children: React.ReactNode;
asChild?: boolean;
}
>;
type IconProps<T> = DefaultIconProps<T> &
Omit<SVGMotionProps<SVGSVGElement>, 'animate'> & {
size?: number;
};
type IconWrapperProps<T> = IconProps<T> & {
icon: React.ComponentType<IconProps<T>>;
};
const AnimateIconContext = React.createContext<AnimateIconContextValue | null>(
null,
);
function useAnimateIconContext() {
const context = React.useContext(AnimateIconContext);
if (!context)
return {
controls: undefined,
animation: 'default',
loop: undefined,
loopDelay: undefined,
active: undefined,
animate: undefined,
initialOnAnimateEnd: undefined,
completeOnStop: undefined,
persistOnAnimateEnd: undefined,
delay: undefined,
};
return context;
}
function composeEventHandlers<E extends React.SyntheticEvent<unknown>>(
theirs?: (event: E) => void,
ours?: (event: E) => void,
) {
return (event: E) => {
theirs?.(event);
ours?.(event);
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyProps = Record<string, any>;
function AnimateIcon({
asChild = false,
animate = false,
animateOnHover = false,
animateOnTap = false,
animateOnView = false,
animateOnViewMargin = '0px',
animateOnViewOnce = true,
animation = 'default',
loop = false,
loopDelay = 0,
initialOnAnimateEnd = false,
completeOnStop = false,
persistOnAnimateEnd = false,
delay = 0,
children,
...props
}: AnimateIconProps) {
const controls = useAnimation();
const [localAnimate, setLocalAnimate] = React.useState<boolean>(() => {
if (animate === undefined || animate === false) return false;
return delay <= 0;
});
const [currentAnimation, setCurrentAnimation] = React.useState<
string | StaticAnimations
>(typeof animate === 'string' ? animate : animation);
const [status, setStatus] = React.useState<'initial' | 'animate'>('initial');
const delayRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const loopDelayRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const isAnimateInProgressRef = React.useRef<boolean>(false);
const animateEndPromiseRef = React.useRef<Promise<void> | null>(null);
const resolveAnimateEndRef = React.useRef<(() => void) | null>(null);
const activeRef = React.useRef<boolean>(localAnimate);
const runGenRef = React.useRef(0);
const cancelledRef = React.useRef(false);
const bumpGeneration = React.useCallback(() => {
runGenRef.current++;
}, []);
const startAnimation = React.useCallback(
(trigger: TriggerProp) => {
const next = typeof trigger === 'string' ? trigger : animation;
bumpGeneration();
if (delayRef.current) {
clearTimeout(delayRef.current);
delayRef.current = null;
}
setCurrentAnimation(next);
if (delay > 0) {
setLocalAnimate(false);
delayRef.current = setTimeout(() => {
setLocalAnimate(true);
}, delay);
} else {
setLocalAnimate(true);
}
},
[animation, delay, bumpGeneration],
);
const stopAnimation = React.useCallback(() => {
bumpGeneration();
if (delayRef.current) {
clearTimeout(delayRef.current);
delayRef.current = null;
}
if (loopDelayRef.current) {
clearTimeout(loopDelayRef.current);
loopDelayRef.current = null;
}
setLocalAnimate(false);
}, [bumpGeneration]);
React.useEffect(() => {
activeRef.current = localAnimate;
}, [localAnimate]);
React.useEffect(() => {
if (animate === undefined) return;
setCurrentAnimation(typeof animate === 'string' ? animate : animation);
if (animate) startAnimation(animate as TriggerProp);
else stopAnimation();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animate]);
React.useEffect(() => {
return () => {
if (delayRef.current) clearTimeout(delayRef.current);
if (loopDelayRef.current) clearTimeout(loopDelayRef.current);
};
}, []);
const viewOuterRef = React.useRef<HTMLElement>(null);
const { ref: inViewRef, isInView } = useIsInView(viewOuterRef, {
inView: !!animateOnView,
inViewOnce: animateOnViewOnce,
inViewMargin: animateOnViewMargin,
});
const startAnim = React.useCallback(
async (anim: 'initial' | 'animate', method: 'start' | 'set' = 'start') => {
try {
await controls[method](anim);
setStatus(anim);
} catch {
return;
}
},
[controls],
);
React.useEffect(() => {
if (!animateOnView) return;
if (isInView) startAnimation(animateOnView);
else stopAnimation();
}, [isInView, animateOnView, startAnimation, stopAnimation]);
React.useEffect(() => {
const gen = ++runGenRef.current;
cancelledRef.current = false;
async function run() {
if (cancelledRef.current || gen !== runGenRef.current) {
await startAnim('initial');
return;
}
if (!localAnimate) {
if (
completeOnStop &&
isAnimateInProgressRef.current &&
animateEndPromiseRef.current
) {
try {
await animateEndPromiseRef.current;
} catch {
// noop
}
}
if (!persistOnAnimateEnd) {
if (cancelledRef.current || gen !== runGenRef.current) {
await startAnim('initial');
return;
}
await startAnim('initial');
}
return;
}
if (loop) {
if (cancelledRef.current || gen !== runGenRef.current) {
await startAnim('initial');
return;
}
await startAnim('initial', 'set');
}
isAnimateInProgressRef.current = true;
animateEndPromiseRef.current = new Promise<void>((resolve) => {
resolveAnimateEndRef.current = resolve;
});
if (cancelledRef.current || gen !== runGenRef.current) {
isAnimateInProgressRef.current = false;
resolveAnimateEndRef.current?.();
resolveAnimateEndRef.current = null;
animateEndPromiseRef.current = null;
await startAnim('initial');
return;
}
await startAnim('animate');
if (cancelledRef.current || gen !== runGenRef.current) {
isAnimateInProgressRef.current = false;
resolveAnimateEndRef.current?.();
resolveAnimateEndRef.current = null;
animateEndPromiseRef.current = null;
await startAnim('initial');
return;
}
isAnimateInProgressRef.current = false;
resolveAnimateEndRef.current?.();
resolveAnimateEndRef.current = null;
animateEndPromiseRef.current = null;
if (initialOnAnimateEnd) {
if (cancelledRef.current || gen !== runGenRef.current) {
await startAnim('initial');
return;
}
await startAnim('initial', 'set');
}
if (loop) {
if (loopDelay > 0) {
await new Promise<void>((resolve) => {
loopDelayRef.current = setTimeout(() => {
loopDelayRef.current = null;
resolve();
}, loopDelay);
});
if (cancelledRef.current || gen !== runGenRef.current) {
await startAnim('initial');
return;
}
if (!activeRef.current) {
if (status !== 'initial' && !persistOnAnimateEnd)
await startAnim('initial');
return;
}
} else {
if (!activeRef.current) {
if (status !== 'initial' && !persistOnAnimateEnd)
await startAnim('initial');
return;
}
}
if (cancelledRef.current || gen !== runGenRef.current) {
await startAnim('initial');
return;
}
await run();
}
}
void run();
return () => {
cancelledRef.current = true;
if (delayRef.current) {
clearTimeout(delayRef.current);
delayRef.current = null;
}
if (loopDelayRef.current) {
clearTimeout(loopDelayRef.current);
loopDelayRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localAnimate, controls]);
const childProps = (
React.isValidElement(children) ? (children as React.ReactElement).props : {}
) as AnyProps;
const handleMouseEnter = composeEventHandlers<React.MouseEvent<HTMLElement>>(
childProps.onMouseEnter,
() => {
if (animateOnHover) startAnimation(animateOnHover);
},
);
const handleMouseLeave = composeEventHandlers<React.MouseEvent<HTMLElement>>(
childProps.onMouseLeave,
() => {
if (animateOnHover || animateOnTap) stopAnimation();
},
);
const handlePointerDown = composeEventHandlers<
React.PointerEvent<HTMLElement>
>(childProps.onPointerDown, () => {
if (animateOnTap) startAnimation(animateOnTap);
});
const handlePointerUp = composeEventHandlers<React.PointerEvent<HTMLElement>>(
childProps.onPointerUp,
() => {
if (animateOnTap) stopAnimation();
},
);
const content = asChild ? (
<Slot
ref={inViewRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
{...props}
>
{children}
</Slot>
) : (
<motion.span
ref={inViewRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
{...props}
>
{children}
</motion.span>
);
return (
<AnimateIconContext.Provider
value={{
controls,
animation: currentAnimation,
loop,
loopDelay,
active: localAnimate,
animate,
initialOnAnimateEnd,
completeOnStop,
delay,
}}
>
{content}
</AnimateIconContext.Provider>
);
}
const pathClassName =
"[&_[stroke-dasharray='1px_1px']]:![stroke-dasharray:1px_0px]";
function IconWrapper<T extends string>({
size = 28,
animation: animationProp,
animate,
animateOnHover,
animateOnTap,
animateOnView,
animateOnViewMargin,
animateOnViewOnce,
icon: IconComponent,
loop,
loopDelay,
persistOnAnimateEnd,
initialOnAnimateEnd,
delay,
completeOnStop,
className,
...props
}: IconWrapperProps<T>) {
const context = React.useContext(AnimateIconContext);
if (context) {
const {
controls,
animation: parentAnimation,
loop: parentLoop,
loopDelay: parentLoopDelay,
active: parentActive,
animate: parentAnimate,
persistOnAnimateEnd: parentPersistOnAnimateEnd,
initialOnAnimateEnd: parentInitialOnAnimateEnd,
delay: parentDelay,
completeOnStop: parentCompleteOnStop,
} = context;
const hasOverrides =
animate !== undefined ||
animateOnHover !== undefined ||
animateOnTap !== undefined ||
animateOnView !== undefined ||
loop !== undefined ||
loopDelay !== undefined ||
initialOnAnimateEnd !== undefined ||
persistOnAnimateEnd !== undefined ||
delay !== undefined ||
completeOnStop !== undefined;
if (hasOverrides) {
const inheritedAnimate: Trigger = parentActive
? (animationProp ?? parentAnimation ?? 'default')
: false;
const finalAnimate: Trigger = (animate ??
parentAnimate ??
inheritedAnimate) as Trigger;
return (
<AnimateIcon
animate={finalAnimate}
animateOnHover={animateOnHover}
animateOnTap={animateOnTap}
animateOnView={animateOnView}
animateOnViewMargin={animateOnViewMargin}
animateOnViewOnce={animateOnViewOnce}
animation={animationProp ?? parentAnimation}
loop={loop ?? parentLoop}
loopDelay={loopDelay ?? parentLoopDelay}
persistOnAnimateEnd={persistOnAnimateEnd ?? parentPersistOnAnimateEnd}
initialOnAnimateEnd={initialOnAnimateEnd ?? parentInitialOnAnimateEnd}
delay={delay ?? parentDelay}
completeOnStop={completeOnStop ?? parentCompleteOnStop}
asChild
>
<IconComponent
size={size}
className={cn(
className,
((animationProp ?? parentAnimation) === 'path' ||
(animationProp ?? parentAnimation) === 'path-loop') &&
pathClassName,
)}
{...props}
/>
</AnimateIcon>
);
}
const animationToUse = animationProp ?? parentAnimation;
const loopToUse = parentLoop;
const loopDelayToUse = parentLoopDelay;
return (
<AnimateIconContext.Provider
value={{
controls,
animation: animationToUse,
loop: loopToUse,
loopDelay: loopDelayToUse,
active: parentActive,
animate: parentAnimate,
initialOnAnimateEnd: parentInitialOnAnimateEnd,
delay: parentDelay,
completeOnStop: parentCompleteOnStop,
}}
>
<IconComponent
size={size}
className={cn(
className,
(animationToUse === 'path' || animationToUse === 'path-loop') &&
pathClassName,
)}
{...props}
/>
</AnimateIconContext.Provider>
);
}
if (
animate !== undefined ||
animateOnHover !== undefined ||
animateOnTap !== undefined ||
animateOnView !== undefined ||
animationProp !== undefined
) {
return (
<AnimateIcon
animate={animate}
animateOnHover={animateOnHover}
animateOnTap={animateOnTap}
animateOnView={animateOnView}
animateOnViewMargin={animateOnViewMargin}
animateOnViewOnce={animateOnViewOnce}
animation={animationProp}
loop={loop}
loopDelay={loopDelay}
delay={delay}
completeOnStop={completeOnStop}
asChild
>
<IconComponent
size={size}
className={cn(
className,
(animationProp === 'path' || animationProp === 'path-loop') &&
pathClassName,
)}
{...props}
/>
</AnimateIcon>
);
}
return (
<IconComponent
size={size}
className={cn(
className,
(animationProp === 'path' || animationProp === 'path-loop') &&
pathClassName,
)}
{...props}
/>
);
}
function getVariants<
V extends { default: T; [key: string]: T },
T extends Record<string, Variants>,
>(animations: V): T {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { animation: animationType } = useAnimateIconContext();
let result: T;
if (animationType in staticAnimations) {
const variant = staticAnimations[animationType as StaticAnimations];
result = {} as T;
for (const key in animations.default) {
if (
(animationType === 'path' || animationType === 'path-loop') &&
key.includes('group')
)
continue;
result[key] = variant as T[Extract<keyof T, string>];
}
} else {
result = (animations[animationType as keyof V] as T) ?? animations.default;
}
return result;
}
export {
pathClassName,
staticAnimations,
AnimateIcon,
IconWrapper,
useAnimateIconContext,
getVariants,
type IconProps,
type IconWrapperProps,
type AnimateIconProps,
type AnimateIconContextValue,
};
@@ -0,0 +1,87 @@
'use client';
import * as React from 'react';
import {motion, type Variants} from 'motion/react';
import {getVariants, type IconProps, IconWrapper, useAnimateIconContext,} from '@/components/animate-ui/icons/icon';
type SendProps = IconProps<keyof typeof animations>;
const animations = {
default: {
group: {
initial: {
scale: 1,
x: 0,
y: 0,
},
animate: {
scale: [1, 0.8, 1, 1, 1],
x: [0, '-10%', '100%', '-125%', 0],
y: [0, '10%', '-100%', '125%', 0],
transition: {
default: { ease: 'easeInOut', duration: 1.2 },
x: {
ease: 'easeInOut',
duration: 1.2,
times: [0, 0.25, 0.5, 0.5, 1],
},
y: {
ease: 'easeInOut',
duration: 1.2,
times: [0, 0.25, 0.5, 0.5, 1],
},
},
},
},
path1: {},
path2: {},
} satisfies Record<string, Variants>,
} as const;
function IconComponent({ size, ...props }: SendProps) {
const { controls } = useAnimateIconContext();
const variants = getVariants(animations);
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<motion.g variants={variants.group} initial="initial" animate={controls}>
<motion.path
d="M14.5,21.7c.1.3.4.4.7.3.1,0,.2-.2.3-.3L22,2.7c0-.3,0-.5-.3-.6-.1,0-.2,0-.3,0L2.3,8.5c-.3,0-.4.4-.3.6,0,.1.2.2.3.3l7.9,3.2c.5.2.9.6,1.1,1.1l3.2,7.9Z"
variants={variants.path1}
initial="initial"
animate={controls}
/>
<motion.path
d="M21.9,2.1l-10.9,10.9"
variants={variants.path2}
initial="initial"
animate={controls}
/>
</motion.g>
</motion.svg>
);
}
function Send(props: SendProps) {
return <IconWrapper icon={IconComponent} {...props} />;
}
export {
animations,
Send,
Send as SendIcon,
type SendProps,
type SendProps as SendIconProps,
};
@@ -0,0 +1,96 @@
'use client';
import * as React from 'react';
import {type HTMLMotionProps, isMotionComponent, motion} from 'motion/react';
import {cn} from '@/lib/utils';
type AnyProps = Record<string, unknown>;
type DOMMotionProps<T extends HTMLElement = HTMLElement> = Omit<
HTMLMotionProps<keyof HTMLElementTagNameMap>,
'ref'
> & { ref?: React.Ref<T> };
type WithAsChild<Base extends object> =
| (Base & { asChild: true; children: React.ReactElement })
| (Base & { asChild?: false | undefined });
type SlotProps<T extends HTMLElement = HTMLElement> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children?: any;
} & DOMMotionProps<T>;
function mergeRefs<T>(
...refs: (React.Ref<T> | undefined)[]
): React.RefCallback<T> {
return (node) => {
refs.forEach((ref) => {
if (!ref) return;
if (typeof ref === 'function') {
ref(node);
} else {
(ref as React.RefObject<T | null>).current = node;
}
});
};
}
function mergeProps<T extends HTMLElement>(
childProps: AnyProps,
slotProps: DOMMotionProps<T>,
): AnyProps {
const merged: AnyProps = { ...childProps, ...slotProps };
if (childProps.className || slotProps.className) {
merged.className = cn(
childProps.className as string,
slotProps.className as string,
);
}
if (childProps.style || slotProps.style) {
merged.style = {
...(childProps.style as React.CSSProperties),
...(slotProps.style as React.CSSProperties),
};
}
return merged;
}
function Slot<T extends HTMLElement = HTMLElement>({
children,
ref,
...props
}: SlotProps<T>) {
const isAlreadyMotion =
typeof children.type === 'object' &&
children.type !== null &&
isMotionComponent(children.type);
const Base = React.useMemo(
() =>
isAlreadyMotion
? (children.type as React.ElementType)
: motion.create(children.type as React.ElementType),
[isAlreadyMotion, children.type],
);
if (!React.isValidElement(children)) return null;
const { ref: childRef, ...childProps } = children.props as AnyProps;
const mergedProps = mergeProps(childProps, props);
return (
<Base {...mergedProps} ref={mergeRefs(childRef as React.Ref<T>, ref)} />
);
}
export {
Slot,
type SlotProps,
type WithAsChild,
type DOMMotionProps,
type AnyProps,
};
@@ -0,0 +1,32 @@
'use client';
import * as React from 'react';
import {type HTMLMotionProps, motion} from 'motion/react';
import {Slot, type WithAsChild} from '@/components/animate-ui/primitives/animate/slot';
type ButtonProps = WithAsChild<
HTMLMotionProps<'button'> & {
hoverScale?: number;
tapScale?: number;
}
>;
function Button({
hoverScale = 1.05,
tapScale = 0.95,
asChild = false,
...props
}: ButtonProps) {
const Component = asChild ? Slot : motion.button;
return (
<Component
whileTap={{ scale: tapScale }}
whileHover={{ scale: hoverScale }}
{...props}
/>
);
}
export { Button, type ButtonProps };
@@ -0,0 +1,144 @@
'use client';
import * as React from 'react';
import {type HTMLMotionProps, motion} from 'motion/react';
import {getStrictContext} from '@/lib/utils/get-strict-context';
import {Slot, type WithAsChild} from '@/components/animate-ui/primitives/animate/slot';
type Ripple = {
id: number;
x: number;
y: number;
};
type RippleButtonContextType = {
ripples: Ripple[];
setRipples: (ripples: Ripple[]) => void;
};
const [RippleButtonProvider, useRippleButton] =
getStrictContext<RippleButtonContextType>('RippleButtonContext');
type RippleButtonProps = WithAsChild<
HTMLMotionProps<'button'> & {
hoverScale?: number;
tapScale?: number;
}
>;
function RippleButton({
ref,
onClick,
hoverScale = 1,
tapScale = 0.95,
asChild = false,
style,
...props
}: RippleButtonProps) {
const [ripples, setRipples] = React.useState<Ripple[]>([]);
const buttonRef = React.useRef<HTMLButtonElement>(null);
React.useImperativeHandle(ref, () => buttonRef.current as HTMLButtonElement);
const createRipple = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
const button = buttonRef.current;
if (!button) return;
const rect = button.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const newRipple: Ripple = {
id: Date.now(),
x,
y,
};
setRipples((prev) => [...prev, newRipple]);
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
}, 600);
},
[],
);
const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
createRipple(event);
if (onClick) {
onClick(event);
}
},
[createRipple, onClick],
);
const Component = asChild ? Slot : motion.button;
return (
<RippleButtonProvider value={{ ripples, setRipples }}>
<Component
ref={buttonRef}
data-slot="ripple-button"
onClick={handleClick}
whileTap={{ scale: tapScale }}
whileHover={{ scale: hoverScale }}
style={{
position: 'relative',
overflow: 'hidden',
...style,
}}
{...props}
/>
</RippleButtonProvider>
);
}
type RippleButtonRipplesProps = WithAsChild<
HTMLMotionProps<'span'> & {
color?: string;
scale?: number;
}
>;
function RippleButtonRipples({
color = 'var(--ripple-button-ripple-color)',
scale = 10,
transition = { duration: 0.6, ease: 'easeOut' },
asChild = false,
style,
...props
}: RippleButtonRipplesProps) {
const { ripples } = useRippleButton();
const Component = asChild ? Slot : motion.span;
return ripples.map((ripple) => (
<Component
key={ripple.id}
initial={{ scale: 0, opacity: 0.5 }}
animate={{ scale, opacity: 0 }}
transition={transition}
style={{
position: 'absolute',
borderRadius: '50%',
pointerEvents: 'none',
width: '20px',
height: '20px',
backgroundColor: color,
top: ripple.y - 10,
left: ripple.x - 10,
...style,
}}
{...props}
/>
));
}
export {
RippleButton,
RippleButtonRipples,
type RippleButtonProps,
type RippleButtonRipplesProps,
};
@@ -0,0 +1,110 @@
'use client';
import * as React from 'react';
import {Collapsible as CollapsiblePrimitive} from 'radix-ui';
import {AnimatePresence, type HTMLMotionProps, motion} from 'motion/react';
import {getStrictContext} from '@/lib/utils/get-strict-context';
import {useControlledState} from '@/hooks/use-controlled-state';
type CollapsibleContextType = {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
};
const [CollapsibleProvider, useCollapsible] =
getStrictContext<CollapsibleContextType>('CollapsibleContext');
type CollapsibleProps = React.ComponentProps<typeof CollapsiblePrimitive.Root>;
function Collapsible(props: CollapsibleProps) {
const [isOpen, setIsOpen] = useControlledState({
value: props?.open,
defaultValue: props?.defaultOpen,
onChange: props?.onOpenChange,
});
return (
<CollapsibleProvider value={{ isOpen, setIsOpen }}>
<CollapsiblePrimitive.Root
data-slot="collapsible"
{...props}
onOpenChange={setIsOpen}
/>
</CollapsibleProvider>
);
}
type CollapsibleTriggerProps = React.ComponentProps<
typeof CollapsiblePrimitive.Trigger
>;
function CollapsibleTrigger(props: CollapsibleTriggerProps) {
return (
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
);
}
type CollapsibleContentProps = Omit<
React.ComponentProps<typeof CollapsiblePrimitive.Content>,
'asChild' | 'forceMount'
> &
HTMLMotionProps<'div'> & {
keepRendered?: boolean;
};
function CollapsibleContent({
keepRendered = false,
transition = { duration: 0.35, ease: 'easeInOut' },
...props
}: CollapsibleContentProps) {
const { isOpen } = useCollapsible();
return (
<AnimatePresence>
{keepRendered ? (
<CollapsiblePrimitive.Content asChild forceMount>
<motion.div
key="collapsible-content"
data-slot="collapsible-content"
layout
initial={{ opacity: 0, height: 0, overflow: 'hidden', y: 20 }}
animate={
isOpen
? { opacity: 1, height: 'auto', overflow: 'hidden', y: 0 }
: { opacity: 0, height: 0, overflow: 'hidden', y: 20 }
}
transition={transition}
{...props}
/>
</CollapsiblePrimitive.Content>
) : (
isOpen && (
<CollapsiblePrimitive.Content asChild forceMount>
<motion.div
key="collapsible-content"
data-slot="collapsible-content"
layout
initial={{ opacity: 0, height: 0, overflow: 'hidden', y: 20 }}
animate={{ opacity: 1, height: 'auto', overflow: 'hidden', y: 0 }}
exit={{ opacity: 0, height: 0, overflow: 'hidden', y: 20 }}
transition={transition}
{...props}
/>
</CollapsiblePrimitive.Content>
)
)}
</AnimatePresence>
);
}
export {
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
useCollapsible,
type CollapsibleProps,
type CollapsibleTriggerProps,
type CollapsibleContentProps,
type CollapsibleContextType,
};
@@ -0,0 +1,133 @@
'use client';
import * as React from 'react';
import {type SpringOptions, useMotionValue, useSpring} from 'motion/react';
import {useIsInView, type UseIsInViewOptions,} from '@/hooks/use-is-in-view';
type CountingNumberProps = Omit<React.ComponentProps<'span'>, 'children'> & {
number: number;
fromNumber?: number;
padStart?: boolean;
decimalSeparator?: string;
decimalPlaces?: number;
transition?: SpringOptions;
delay?: number;
initiallyStable?: boolean;
thousandSeparator?: string;
} & UseIsInViewOptions;
function CountingNumber({
ref,
number,
fromNumber = 0,
padStart = false,
inView = false,
inViewMargin = '0px',
inViewOnce = true,
decimalSeparator = '.',
transition = { stiffness: 200, damping: 30 },
decimalPlaces = 0,
delay = 0,
initiallyStable = false,
thousandSeparator = '',
...props
}: CountingNumberProps) {
const { ref: localRef, isInView } = useIsInView(
ref as React.Ref<HTMLElement>,
{
inView,
inViewOnce,
inViewMargin,
},
);
const numberStr = number.toString();
const decimals =
typeof decimalPlaces === 'number'
? decimalPlaces
: numberStr.includes('.')
? (numberStr.split('.')[1]?.length ?? 0)
: 0;
const motionVal = useMotionValue(initiallyStable ? number : fromNumber);
const springVal = useSpring(motionVal, transition);
React.useEffect(() => {
const timeoutId = setTimeout(() => {
if (isInView) motionVal.set(number);
}, delay);
return () => clearTimeout(timeoutId);
}, [isInView, number, motionVal, delay]);
React.useEffect(() => {
const unsubscribe = springVal.on('change', (latest) => {
if (localRef.current) {
let formatted =
decimals > 0
? latest.toFixed(decimals)
: Math.round(latest).toString();
if (decimals > 0) {
formatted = formatted.replace('.', decimalSeparator);
}
if (padStart) {
const finalIntLength = Math.floor(Math.abs(number)).toString().length;
const [intPart, fracPart] = formatted.split(decimalSeparator);
const paddedInt = intPart?.padStart(finalIntLength, '0') ?? '';
formatted = fracPart
? `${ paddedInt }${ decimalSeparator }${ fracPart }`
: paddedInt;
}
if (thousandSeparator) {
const [intPart, fracPart] = formatted.split(decimalSeparator);
const regex = /\B(?=(\d{3})+(?!\d))/g;
const separatedInt = intPart.replace(regex, thousandSeparator);
formatted = fracPart
? `${ separatedInt }${ decimalSeparator }${ fracPart }`
: separatedInt;
}
localRef.current.textContent = formatted;
}
});
return () => unsubscribe();
}, [springVal, decimals, padStart, number, decimalSeparator, localRef, thousandSeparator]);
const finalIntLength = Math.floor(Math.abs(number)).toString().length;
const formatValue = (val: number) => {
let out = decimals > 0 ? val.toFixed(decimals) : Math.round(val).toString();
if (decimals > 0) out = out.replace('.', decimalSeparator);
if (padStart) {
const [intPart, fracPart] = out.split(decimalSeparator);
const paddedInt = (intPart ?? '').padStart(finalIntLength, '0');
out = fracPart ? `${ paddedInt }${ decimalSeparator }${ fracPart }` : paddedInt;
}
if (thousandSeparator) {
const [intPart, fracPart] = out.split(decimalSeparator);
const regex = /\B(?=(\d{3})+(?!\d))/g;
const separatedInt = intPart.replace(regex, thousandSeparator);
out = fracPart ? `${ separatedInt }${ decimalSeparator }${ fracPart }` : separatedInt;
}
return out;
};
const zeroText = padStart
? '0'.padStart(finalIntLength, '0') +
(decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : '')
: '0' + (decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : '');
const initialText = initiallyStable ? formatValue(number) : zeroText;
return (
<span ref={localRef} data-slot="counting-number" {...props}>
{initialText}
</span>
);
}
export { CountingNumber, type CountingNumberProps };