|
<script setup lang="ts"> |
|
import { ref, watchEffect, type HTMLAttributes } from "vue"; |
|
import { Primitive, type PrimitiveProps } from 'reka-ui' |
|
import { cn } from '@/lib/shadcn/utils' |
|
import { type ButtonVariants, buttonVariants } from '.' |
|
interface Props extends PrimitiveProps { |
|
variant?: ButtonVariants['variant'] |
|
size?: ButtonVariants['size'] |
|
class?: HTMLAttributes['class'] |
|
ripple?: boolean |
|
rippleColorClass?: string; |
|
duration?: number; |
|
} |
|
const props = withDefaults(defineProps<Props>(), { |
|
as: 'button', |
|
rippleColorClass: "bg-accent-foreground/30", |
|
duration: 600, |
|
}) |
|
const buttonRef = ref<{$el: HTMLButtonElement} | null>(null); |
|
const buttonRipples = ref<Array<{ x: number; y: number; size: number; key: number }>>([]); |
|
function createRipple(event: MouseEvent) { |
|
const button = buttonRef.value?.$el; |
|
if (!button) return; |
|
const rect = button.getBoundingClientRect(); |
|
const size = Math.max(rect.width, rect.height); |
|
const x = event.clientX - rect.left - size / 2; |
|
const y = event.clientY - rect.top - size / 2; |
|
const newRipple = { x, y, size, key: Date.now() }; |
|
buttonRipples.value.push(newRipple); |
|
} |
|
watchEffect(() => { |
|
if (buttonRipples.value.length > 0) { |
|
const lastRipple = buttonRipples.value[buttonRipples.value.length - 1]!; |
|
setTimeout(() => { |
|
buttonRipples.value = buttonRipples.value.filter((ripple) => ripple.key !== lastRipple.key); |
|
}, props.duration); |
|
} |
|
}); |
|
</script> |
|
|
|
<template> |
|
<Primitive |
|
ref="buttonRef" |
|
:as="as" |
|
:as-child="asChild" |
|
:class="cn(buttonVariants({ variant, size }), 'relative overflow-hidden', props.class)" |
|
:style="{ '--duration': $props.duration + 'ms' }" |
|
@pointerdown="$props.ripple && createRipple($event)" |
|
> |
|
<span class="pointer-events-none absolute inset-0"> |
|
<span |
|
v-for="ripple in buttonRipples" |
|
:key="ripple.key" |
|
class="ripple-animation absolute rounded-full opacity-30" |
|
:class="[$props.rippleColorClass]" |
|
:style="{ |
|
width: ripple.size + 'px', |
|
height: ripple.size + 'px', |
|
top: ripple.y + 'px', |
|
left: ripple.x + 'px', |
|
transform: 'scale(0)', |
|
animationDuration: $props.duration + 'ms', |
|
}" |
|
/> |
|
</span> |
|
<slot /> |
|
</Primitive> |
|
</template> |
|
|
|
<style scoped> |
|
@keyframes rippling { |
|
0% { |
|
opacity: 1; |
|
} |
|
100% { |
|
transform: scale(2); |
|
opacity: 0; |
|
} |
|
} |
|
.ripple-animation { |
|
animation: rippling var(--duration) ease-out; |
|
} |
|
</style> |
Credits: shadcn/vue and InspiraUI