mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix: rewrite HoverBorderGradient with CSS rotate instead of framer-motion
Replace framer-motion gradient interpolation (buggy in TG WebView) with pure CSS conic-gradient + transform:rotate() keyframe animation. Transform animations are GPU-accelerated and reliable everywhere.
This commit is contained in:
@@ -1,95 +1,44 @@
|
|||||||
'use client';
|
import React from 'react';
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type Direction = 'TOP' | 'LEFT' | 'BOTTOM' | 'RIGHT';
|
|
||||||
|
|
||||||
export function HoverBorderGradient({
|
export function HoverBorderGradient({
|
||||||
children,
|
children,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
className,
|
className,
|
||||||
as: Tag = 'button',
|
as: Tag = 'button',
|
||||||
duration = 1,
|
|
||||||
clockwise = true,
|
|
||||||
...props
|
...props
|
||||||
}: React.PropsWithChildren<
|
}: React.PropsWithChildren<
|
||||||
{
|
{
|
||||||
as?: React.ElementType;
|
as?: React.ElementType;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
duration?: number;
|
|
||||||
clockwise?: boolean;
|
|
||||||
} & React.HTMLAttributes<HTMLElement>
|
} & React.HTMLAttributes<HTMLElement>
|
||||||
>) {
|
>) {
|
||||||
const [hovered, setHovered] = useState<boolean>(false);
|
|
||||||
const [direction, setDirection] = useState<Direction>('TOP');
|
|
||||||
|
|
||||||
const rotateDirection = (currentDirection: Direction): Direction => {
|
|
||||||
const directions: Direction[] = ['TOP', 'LEFT', 'BOTTOM', 'RIGHT'];
|
|
||||||
const currentIndex = directions.indexOf(currentDirection);
|
|
||||||
const nextIndex = clockwise
|
|
||||||
? (currentIndex - 1 + directions.length) % directions.length
|
|
||||||
: (currentIndex + 1) % directions.length;
|
|
||||||
return directions[nextIndex];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bright accent-colored moving gradient spot
|
|
||||||
const movingMap: Record<Direction, string> = {
|
|
||||||
TOP: 'radial-gradient(30% 60% at 50% 0%, rgb(var(--color-accent-400)) 0%, transparent 100%)',
|
|
||||||
LEFT: 'radial-gradient(25% 55% at 0% 50%, rgb(var(--color-accent-400)) 0%, transparent 100%)',
|
|
||||||
BOTTOM:
|
|
||||||
'radial-gradient(30% 60% at 50% 100%, rgb(var(--color-accent-400)) 0%, transparent 100%)',
|
|
||||||
RIGHT:
|
|
||||||
'radial-gradient(25% 55% at 100% 50%, rgb(var(--color-accent-400)) 0%, transparent 100%)',
|
|
||||||
};
|
|
||||||
|
|
||||||
// On hover — full bright glow
|
|
||||||
const highlight =
|
|
||||||
'radial-gradient(80% 200% at 50% 50%, rgb(var(--color-accent-500)) 0%, rgb(var(--color-accent-300) / 0.3) 50%, transparent 100%)';
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hovered) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setDirection((prevState) => rotateDirection(prevState));
|
|
||||||
}, duration * 1000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [hovered, duration]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
onMouseEnter={() => setHovered(true)}
|
|
||||||
onMouseLeave={() => setHovered(false)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex content-center items-center justify-center overflow-visible rounded-xl border-none p-[1.5px] transition duration-500',
|
'group relative flex items-center justify-center overflow-hidden rounded-xl p-[1.5px]',
|
||||||
containerClassName,
|
containerClassName,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
{/* Rotating conic gradient — pure CSS transform, always smooth */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-[-50%] z-0 animate-border-spin"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'conic-gradient(from 0deg, transparent 0%, transparent 40%, rgb(var(--color-accent-400)) 50%, transparent 60%, transparent 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Inner content */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-10 w-auto rounded-[inherit] bg-dark-900 px-4 py-2.5 text-sm font-medium text-white',
|
'relative z-10 w-auto rounded-[inherit] bg-dark-900 px-4 py-2.5 text-sm font-medium text-white transition-colors duration-300 group-hover:bg-dark-850',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 z-0 flex-none overflow-hidden rounded-[inherit]"
|
|
||||||
style={{
|
|
||||||
filter: 'blur(3px)',
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
initial={{ background: movingMap[direction] }}
|
|
||||||
animate={{
|
|
||||||
background: hovered ? [movingMap[direction], highlight] : movingMap[direction],
|
|
||||||
}}
|
|
||||||
transition={{ ease: 'linear', duration: duration ?? 1 }}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-[2px] z-[1] flex-none rounded-[10px] bg-dark-900" />
|
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,8 +173,13 @@ export default {
|
|||||||
'move-horizontal': 'moveHorizontal 40s ease infinite',
|
'move-horizontal': 'moveHorizontal 40s ease infinite',
|
||||||
'move-in-circle-fast': 'moveInCircle 20s ease infinite',
|
'move-in-circle-fast': 'moveInCircle 20s ease infinite',
|
||||||
'spotlight-ace': 'spotlightAce 2s ease 0.75s 1 forwards',
|
'spotlight-ace': 'spotlightAce 2s ease 0.75s 1 forwards',
|
||||||
|
'border-spin': 'borderSpin 3s linear infinite',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
|
borderSpin: {
|
||||||
|
'0%': { transform: 'rotate(0deg)' },
|
||||||
|
'100%': { transform: 'rotate(360deg)' },
|
||||||
|
},
|
||||||
fadeIn: {
|
fadeIn: {
|
||||||
'0%': { opacity: '0' },
|
'0%': { opacity: '0' },
|
||||||
'100%': { opacity: '1' },
|
'100%': { opacity: '1' },
|
||||||
|
|||||||
Reference in New Issue
Block a user