mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
wavelet init
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user