Moving Border Example 02

PreviousNext

Animated gradient border that smoothly travels around any element, from rounded rectangles to perfect circles.

Docs
phucbmcomponent

Preview

Loading preview…
registry/phucbm/blocks/moving-border/example-02.transformed.tsx
'use client';

import {MovingBorder} from "@/components/phucbm/moving-border";
import {useEffect, useRef, useState} from "react";

export default function MovingBorderExample_02() {
    const [isCircle, setIsCircle] = useState(true);
    const [radius, setRadius] = useState(10);
    const [borderWidth, setBorderWidth] = useState(4);
    const [gradientWidth, setGradientWidth] = useState(150);
    const [duration, setDuration] = useState(4);
    const [colors, setColors] = useState(["#84b5ff", "#dad7f8", "#cb92ff"]);
    const [newColor, setNewColor] = useState("#ff6b6b");

    // Debounced values for rendering
    const [debouncedRadius, setDebouncedRadius] = useState(radius);
    const [debouncedBorderWidth, setDebouncedBorderWidth] = useState(borderWidth);
    const [debouncedGradientWidth, setDebouncedGradientWidth] = useState(gradientWidth);
    const [debouncedDuration, setDebouncedDuration] = useState(duration);

    // Debounce timers
    const radiusTimer = useRef<NodeJS.Timeout | null>(null);
    const borderWidthTimer = useRef<NodeJS.Timeout | null>(null);
    const gradientWidthTimer = useRef<NodeJS.Timeout | null>(null);
    const durationTimer = useRef<NodeJS.Timeout | null>(null);

    // Debounce radius
    useEffect(() => {
        if (radiusTimer.current) clearTimeout(radiusTimer.current);
        radiusTimer.current = setTimeout(() => {
            setDebouncedRadius(radius);
        }, 100);
        return () => {
            if (radiusTimer.current) clearTimeout(radiusTimer.current);
        };
    }, [radius]);

    // Debounce border width
    useEffect(() => {
        if (borderWidthTimer.current) clearTimeout(borderWidthTimer.current);
        borderWidthTimer.current = setTimeout(() => {
            setDebouncedBorderWidth(borderWidth);
        }, 100);
        return () => {
            if (borderWidthTimer.current) clearTimeout(borderWidthTimer.current);
        };
    }, [borderWidth]);

    // Debounce gradient width
    useEffect(() => {
        if (gradientWidthTimer.current) clearTimeout(gradientWidthTimer.current);
        gradientWidthTimer.current = setTimeout(() => {
            setDebouncedGradientWidth(gradientWidth);
        }, 100);
        return () => {
            if (gradientWidthTimer.current) clearTimeout(gradientWidthTimer.current);
        };
    }, [gradientWidth]);

    // Debounce duration
    useEffect(() => {
        if (durationTimer.current) clearTimeout(durationTimer.current);
        durationTimer.current = setTimeout(() => {
            setDebouncedDuration(duration);
        }, 100);
        return () => {
            if (durationTimer.current) clearTimeout(durationTimer.current);
        };
    }, [duration]);

    const handleAddColor = () => {
        if (colors.length < 10) {
            setColors([...colors, newColor]);
        }
    };

    const handleRemoveColor = (index: number) => {
        if (colors.length > 1) {
            setColors(colors.filter((_, i) => i !== index));
        }
    };

    const handleColorChange = (index: number, value: string) => {
        const updatedColors = [...colors];
        updatedColors[index] = value;
        setColors(updatedColors);
    };

    return (
        <div className="flex justify-center items-center flex-wrap h-screen bg-emerald-50">
            <div className="w-[calc(100%-240px)] flex justify-center items-center">
                {/* Moving Border Element */}
                <MovingBorder
                    key={isCircle ? 'circle' : 'square'}
                    isCircle={isCircle}
                    {...(!isCircle && {radius: debouncedRadius})}
                    borderWidth={debouncedBorderWidth}
                    gradientWidth={debouncedGradientWidth}
                    duration={debouncedDuration}
                    colors={colors}
                >
                    <div
                        className="w-[200px] aspect-square bg-accent overflow-hidden flex justify-center items-center"
                        style={{
                            borderRadius: isCircle ? '50%' : `${debouncedRadius}px`
                        }}
                    >
                        <img
                            className="object-cover w-full h-full"
                            src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExcGJrZ3NlejZ4ZXlvaDRnbTR1b2VmcG1waGM1Y3hvNGU4aGE0aHcweSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/26BkNnO2qmCWTQuac/giphy.gif"
                            alt="A blurry photo of white flowers in a field"
                        />
                    </div>
                </MovingBorder>
            </div>

            {/* Control Panel */}
            <div className="w-[240px] p-3">
                <div className="bg-white rounded-xl p-3 shadow-xl max-h-[95vh] overflow-y-auto custom-scrollbar">
                    {/* Shape Toggle Section */}
                    <div className="mb-4">
                        <div className="text-[0.7rem] font-semibold text-gray-400 uppercase tracking-wider mb-2">
                            Shape
                        </div>
                        <div className="flex items-center justify-between">
                            <span className="text-sm text-gray-900 font-medium">Circle Mode</span>
                            <button
                                onClick={() => setIsCircle(!isCircle)}
                                className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
                                    isCircle ? 'bg-emerald-500' : 'bg-gray-200'
                                }`}
                            >
                            <span
                                className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
                                    isCircle ? 'translate-x-6' : 'translate-x-1'
                                }`}
                            />
                            </button>
                        </div>
                    </div>

                    <div className="h-px bg-gray-200 my-3"/>

                    {/* Parameters Section */}
                    <div>
                        <div className="text-[0.7rem] font-semibold text-gray-400 uppercase tracking-wider mb-2">
                            Parameters
                        </div>

                        {/* Radius Control - Only shown when not circle */}
                        {!isCircle && (
                            <div className="mb-2">
                                <label className="flex justify-between text-[0.7rem] text-gray-900 mb-1 font-medium">
                                    <span>Radius</span>
                                    <span className="font-semibold text-emerald-500">{radius}px</span>
                                </label>
                                <input
                                    type="range"
                                    min="1"
                                    max="50"
                                    step="1"
                                    value={radius}
                                    onChange={(e) => setRadius(Number(e.target.value))}
                                    className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
                                />
                                <p className="text-[0.65rem] text-gray-400 mt-1.5 leading-tight">
                                    Border radius for corners
                                </p>
                            </div>
                        )}

                        {/* Border Width Control */}
                        <div className="mb-2">
                            <label className="flex justify-between text-[0.7rem] text-gray-900 mb-1 font-medium">
                                <span>Border Width</span>
                                <span className="font-semibold text-emerald-500">{borderWidth}px</span>
                            </label>
                            <input
                                type="range"
                                min="1"
                                max="50"
                                step="1"
                                value={borderWidth}
                                onChange={(e) => setBorderWidth(Number(e.target.value))}
                                className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
                            />
                            <p className="text-[0.65rem] text-gray-400 mt-1.5 leading-tight">
                                Thickness of the border
                            </p>
                        </div>

                        {/* Gradient Width Control */}
                        <div className="mb-2">
                            <label className="flex justify-between text-[0.7rem] text-gray-900 mb-1 font-medium">
                                <span>Gradient Width</span>
                                <span className="font-semibold text-emerald-500">{gradientWidth}px</span>
                            </label>
                            <input
                                type="range"
                                min="10"
                                max="400"
                                step="10"
                                value={gradientWidth}
                                onChange={(e) => setGradientWidth(Number(e.target.value))}
                                className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
                            />
                            <p className="text-[0.65rem] text-gray-400 mt-1.5 leading-tight">
                                Width of gradient effect
                            </p>
                        </div>

                        {/* Duration Control */}
                        <div className="mb-2">
                            <label className="flex justify-between text-[0.7rem] text-gray-900 mb-1 font-medium">
                                <span>Duration</span>
                                <span className="font-semibold text-emerald-500">{duration}s</span>
                            </label>
                            <input
                                type="range"
                                min="1"
                                max="20"
                                step="0.5"
                                value={duration}
                                onChange={(e) => setDuration(Number(e.target.value))}
                                className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
                            />
                            <p className="text-[0.65rem] text-gray-400 mt-1.5 leading-tight">
                                Animation speed (lower=faster)
                            </p>
                        </div>
                    </div>

                    <div className="h-px bg-gray-200 my-3"/>

                    {/* Colors Section */}
                    <div>
                        <div className="text-[0.7rem] font-semibold text-gray-400 uppercase tracking-wider mb-2">
                            Colors
                        </div>

                        {/* Color List */}
                        <div className="space-y-2 mb-3">
                            {colors.map((color, index) => (
                                <div key={index} className="flex items-center gap-2">
                                    <input
                                        type="color"
                                        value={color}
                                        onChange={(e) => handleColorChange(index, e.target.value)}
                                        className="w-8 aspect-square rounded cursor-pointer border border-gray-200"
                                    />
                                    <input
                                        type="text"
                                        value={color}
                                        onChange={(e) => handleColorChange(index, e.target.value)}
                                        className="w-full text-xs px-2 py-1.5 border border-gray-200 rounded font-mono"
                                    />
                                    <button
                                        onClick={() => handleRemoveColor(index)}
                                        disabled={colors.length === 1}
                                        className={`p-1.5 rounded ${
                                            colors.length === 1
                                                ? 'text-gray-300 cursor-not-allowed'
                                                : 'text-red-500 hover:bg-red-50'
                                        }`}
                                    >
                                        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                                                  d="M6 18L18 6M6 6l12 12"/>
                                        </svg>
                                    </button>
                                </div>
                            ))}
                        </div>

                        {/* Add Color */}
                        {colors.length < 10 && (
                            <div className="flex items-center gap-2">
                                <input
                                    type="color"
                                    value={newColor}
                                    onChange={(e) => setNewColor(e.target.value)}
                                    className="w-8 aspect-square rounded cursor-pointer border border-gray-200"
                                />
                                <input
                                    type="text"
                                    value={newColor}
                                    onChange={(e) => setNewColor(e.target.value)}
                                    className="w-full text-xs px-2 py-1.5 border border-gray-200 rounded font-mono"
                                    placeholder="#ff6b6b"
                                />
                                <button
                                    onClick={handleAddColor}
                                    className="p-1.5 rounded text-emerald-500 hover:bg-emerald-50"
                                >
                                    <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
                                              d="M12 4v16m8-8H4"/>
                                    </svg>
                                </button>
                            </div>
                        )}

                        <p className="text-[0.65rem] text-gray-400 mt-2 leading-tight">
                            {colors.length < 10
                                ? `Add up to ${10 - colors.length} more color(s)`
                                : 'Maximum colors reached'}
                        </p>
                    </div>
                </div>

                <style jsx>{`
                    .slider::-webkit-slider-thumb {
                        -webkit-appearance:none;
                        appearance:none;
                        width:12px;
                        height:12px;
                        border-radius:50%;
                        background:#10b981;
                        cursor:pointer;
                        box-shadow:0 1px 3px rgba(0, 0, 0, 0.15);
                    }
                    .slider::-moz-range-thumb {
                        width:12px;
                        height:12px;
                        border-radius:50%;
                        background:#10b981;
                        cursor:pointer;
                        border:none;
                        box-shadow:0 1px 3px rgba(0, 0, 0, 0.15);
                    }

                    /* Scrollbar */
                    .custom-scrollbar::-webkit-scrollbar {
                        width:5px;
                        height:8px; /* Height for horizontal scrollbar */
                    }
                    .custom-scrollbar::-webkit-scrollbar-track {
                        background:#f1f5f9; /* Light gray track */
                        border-radius:4px;
                    }
                    .custom-scrollbar::-webkit-scrollbar-thumb {
                        background:#10b981; /* Emerald green thumb */
                        border-radius:4px;
                    }
                    .custom-scrollbar::-webkit-scrollbar-thumb:hover {
                        background:#059669; /* Darker emerald on hover */
                    }

                    /* Firefox */
                    .custom-scrollbar {
                        scrollbar-width:thin;
                        scrollbar-color:#10b981 #f1f5f9;
                    }
                `}</style>
            </div>
        </div>
    );
}

Installation

npx shadcn@latest add @phucbm/moving-border-example-02

Usage

import { MovingBorderExample02 } from "@/components/moving-border-example-02"
<MovingBorderExample02 />