Design 2 Dev

Copy Code & Use for Framer

Countdown Timer

00:00:10

import * as React from "react"
import { addPropertyControls, ControlType, motion } from "framer"
import { useState, useEffect } from "react"

/**
 * Countdown Timer Component
 */
export default function CountdownTimer(props) {
    const {
        hours,
        minutes,
        seconds,
        showButton,
        buttonText,
        buttonBgColor,
        buttonTextColor,
        buttonBorderRadius,
        buttonBoxShadow,
        buttonHoverBgColor,
        buttonHoverTextColor,
        redirectLink,
        onTimerEnd,
    } = props

    // Convert initial time to total seconds
    const [timeLeft, setTimeLeft] = useState(
        hours * 3600 + minutes * 60 + seconds
    )

    // Timer logic
    useEffect(() => {
        if (timeLeft <= 0) {
            onTimerEnd?.()
            return
        }

        const interval = setInterval(() => {
            setTimeLeft((prev) => prev - 1)
        }, 1000)

        return () => clearInterval(interval)
    }, [timeLeft])

    // Format time into HH:MM:SS
    const formatTime = (totalSeconds) => {
        const hours = Math.floor(totalSeconds / 3600)
        const minutes = Math.floor((totalSeconds % 3600) / 60)
        const seconds = totalSeconds % 60

        return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(
            2,
            "0"
        )}:${String(seconds).padStart(2, "0")}`
    }

    // Handle button click
    const handleButtonClick = () => {
        if (redirectLink) {
            window.open(redirectLink, "_blank")
        }
    }

    return (
        <div style={containerStyle}>
            {timeLeft > 0 ? (
                <h1 style={timerStyle}>{formatTime(timeLeft)}</h1>
            ) : (
                showButton && (
                    <motion.button
                        style={{
                            ...buttonStyle,
                            backgroundColor: buttonBgColor,
                            color: buttonTextColor,
                            borderRadius: `${buttonBorderRadius}px`,
                            boxShadow: buttonBoxShadow,
                        }}
                        whileHover={{
                            backgroundColor: buttonHoverBgColor,
                            color: buttonHoverTextColor,
                        }}
                        onClick={handleButtonClick}
                    >
                        {buttonText}
                    </motion.button>
                )
            )}
        </div>
    )
}

// Default props
CountdownTimer.defaultProps = {
    hours: 0,
    minutes: 0,
    seconds: 10,
    showButton: true,
    buttonText: "Click Me",
    buttonBgColor: "#09F",
    buttonTextColor: "#FFF",
    buttonBorderRadius: 8,
    buttonBoxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
    buttonHoverBgColor: "#007ACC",
    buttonHoverTextColor: "#FFF",
    redirectLink: "https://www.example.com",
}

// Property Controls
addPropertyControls(CountdownTimer, {
    hours: {
        type: ControlType.Number,
        title: "Hours",
        min: 0,
        max: 24,
        defaultValue: 0,
    },
    minutes: {
        type: ControlType.Number,
        title: "Minutes",
        min: 0,
        max: 59,
        defaultValue: 0,
    },
    seconds: {
        type: ControlType.Number,
        title: "Seconds",
        min: 0,
        max: 59,
        defaultValue: 10,
    },
    showButton: {
        type: ControlType.Boolean,
        title: "Show Button",
        defaultValue: true,
    },
    buttonText: {
        type: ControlType.String,
        title: "Button Text",
        defaultValue: "Click Me",
        hidden: (props) => !props.showButton,
    },
    buttonBgColor: {
        type: ControlType.Color,
        title: "Button BG Color",
        defaultValue: "#09F",
        hidden: (props) => !props.showButton,
    },
    buttonTextColor: {
        type: ControlType.Color,
        title: "Button Text Color",
        defaultValue: "#FFF",
        hidden: (props) => !props.showButton,
    },
    buttonBorderRadius: {
        type: ControlType.Number,
        title: "Button Radius",
        min: 0,
        max: 100,
        defaultValue: 8,
        unit: "px",
        hidden: (props) => !props.showButton,
    },
    buttonBoxShadow: {
        type: ControlType.String,
        title: "Button Shadow",
        defaultValue: "0px 4px 6px rgba(0, 0, 0, 0.1)",
        hidden: (props) => !props.showButton,
    },
    buttonHoverBgColor: {
        type: ControlType.Color,
        title: "Hover BG Color",
        defaultValue: "#007ACC",
        hidden: (props) => !props.showButton,
    },
    buttonHoverTextColor: {
        type: ControlType.Color,
        title: "Hover Text Color",
        defaultValue: "#FFF",
        hidden: (props) => !props.showButton,
    },
    redirectLink: {
        type: ControlType.String,
        title: "Redirect Link",
        defaultValue: "https://www.example.com",
        hidden: (props) => !props.showButton,
    },
})

// Styles
const containerStyle = {
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    height: "100%",
    width: "100%",
}

const timerStyle = {
    fontSize: "48px",
    fontWeight: "bold",
    color: "#333",
}

const buttonStyle = {
    padding: "12px 24px",
    border: "none",
    cursor: "pointer",
    fontSize: "16px",
    fontWeight: "bold",
    transition: "background-color 0.3s, color 0.3s",
}

Circling Elements

Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
import * as React from "react"
import { Frame, addPropertyControls, ControlType } from "framer"

type Props = {
    radius: number
    duration: number
    direction: "clockwise" | "counterclockwise"
    imageCount: number
    imageSize: number
    speedUpOnHover: boolean
    // Groupe d'images sous forme de tableau de chaînes (URL)
    images: string[]
}

export function CirclingPictures(props: Props) {
    const {
        radius,
        duration,
        direction,
        imageCount,
        imageSize,
        speedUpOnHover,
        images,
    } = props

    // Calcul du container pour un "fit content" parfait : diamètre du cercle + taille d'une image.
    const containerSize = 2 * radius + imageSize

    // Référence pour le container qui contient les images.
    const containerRef = React.useRef<HTMLDivElement>(null)
    // Stockage de l’offset de rotation (mis à jour en interne sans provoquer de re-render).
    const offsetRef = React.useRef(0)
    // Référence pour le temps de la dernière frame.
    const lastTimeRef = React.useRef<number | null>(null)
    // Multiplicateur dynamique (calculé selon la position du curseur).
    const dynamicMultiplierRef = React.useRef(1)
    // Indique si la souris est sur le composant.
    const isHoveredRef = React.useRef(false)

    // Gestion des événements de la souris pour ajuster le multiplicateur dynamique.
    const handleMouseMove = (
        e: React.MouseEvent<HTMLDivElement, MouseEvent>
    ) => {
        if (!speedUpOnHover) return
        const rect = e.currentTarget.getBoundingClientRect()
        const center = rect.width / 2
        const x = e.clientX - rect.left
        const y = e.clientY - rect.top
        const dx = x - center
        const dy = y - center
        const distance = Math.sqrt(dx * dx + dy * dy)
        const normalized = Math.min(distance / center, 1)
        // Le multiplicateur dynamique varie de 3 (au centre) à 1 (au bord)
        dynamicMultiplierRef.current = 1 + (1 - normalized) * 2
    }

    const handleMouseEnter = () => {
        if (!speedUpOnHover) return
        isHoveredRef.current = true
    }

    const handleMouseLeave = () => {
        if (!speedUpOnHover) return
        dynamicMultiplierRef.current = 1
        isHoveredRef.current = false
    }

    // Boucle d'animation qui met à jour l'offset et les positions des images de façon impérative.
    React.useEffect(() => {
        let animationFrameId: number

        const update = (time: number) => {
            if (lastTimeRef.current !== null) {
                const deltaTime = (time - lastTimeRef.current) / 1000 // en secondes
                const effectiveMultiplier =
                    speedUpOnHover && isHoveredRef.current
                        ? dynamicMultiplierRef.current * 0.75
                        : 1
                // Calcul pour une rotation complète (2π) en "duration" secondes.
                const increment =
                    (2 * Math.PI * deltaTime * effectiveMultiplier) / duration
                const directionFactor = direction === "clockwise" ? 1 : -1
                offsetRef.current += directionFactor * increment

                // Mise à jour impérative des positions des enfants.
                if (containerRef.current) {
                    const children = containerRef.current.children
                    const count = children.length
                    const angleStep = count > 0 ? (2 * Math.PI) / count : 0
                    for (let i = 0; i < count; i++) {
                        const angle = i * angleStep + offsetRef.current
                        const x = radius * Math.cos(angle)
                        const y = radius * Math.sin(angle)
                        const child = children[i] as HTMLElement
                        child.style.left = `calc(50% + ${x}px)`
                        child.style.top = `calc(50% + ${y}px)`
                    }
                }
            }
            lastTimeRef.current = time
            animationFrameId = requestAnimationFrame(update)
        }
        animationFrameId = requestAnimationFrame(update)
        return () => cancelAnimationFrame(animationFrameId)
    }, [duration, direction, radius, speedUpOnHover])

    return (
        <Frame
            width={containerSize}
            height={containerSize}
            background="none"
            style={{ position: "relative", overflow: "visible" }}
            onMouseMove={handleMouseMove}
            onMouseEnter={handleMouseEnter}
            onMouseLeave={handleMouseLeave}
        >
            <div
                ref={containerRef}
                style={{ position: "absolute", width: "100%", height: "100%" }}
            >
                {images.slice(0, imageCount).map((img, index) => (
                    <div
                        key={index}
                        style={{
                            position: "absolute",
                            transform: "translate(-50%, -50%)",
                            width: imageSize,
                            height: imageSize,
                        }}
                    >
                        <img
                            src={img}
                            alt={`Image ${index + 1}`}
                            style={{
                                width: "100%",
                                height: "100%",
                                objectFit: "cover",
                            }}
                        />
                    </div>
                ))}
            </div>
        </Frame>
    )
}

CirclingPictures.defaultProps = {
    radius: 120,
    duration: 10,
    direction: "clockwise",
    imageCount: 7,
    imageSize: 80,
    speedUpOnHover: true,
    images: [
        "https://via.placeholder.com/150/FF0000/FFFFFF?text=Image1",
        "https://via.placeholder.com/150/00FF00/FFFFFF?text=Image2",
        "https://via.placeholder.com/150/0000FF/FFFFFF?text=Image3",
        "https://via.placeholder.com/150/FFFF00/FFFFFF?text=Image4",
        "https://via.placeholder.com/150/FF00FF/FFFFFF?text=Image5",
    ],
}

addPropertyControls(CirclingPictures, {
    radius: {
        type: ControlType.Number,
        title: "Radius",
        min: 0,
        max: 300,
        step: 1,
    },
    duration: {
        type: ControlType.Number,
        title: "Duration (sec)",
        min: 1,
        max: 60,
        step: 1,
    },
    direction: {
        type: ControlType.Enum,
        title: "Direction",
        options: ["clockwise", "counterclockwise"],
    },
    imageCount: {
        type: ControlType.Number,
        title: "Image Count",
        min: 1,
        max: 10,
        step: 1,
    },
    imageSize: {
        type: ControlType.Number,
        title: "Image Size",
        min: 10,
        max: 200,
        step: 1,
    },
    speedUpOnHover: {
        type: ControlType.Boolean,
        title: "Speed up on hover",
    },
    images: {
        type: ControlType.Array,
        title: "Images",
        control: {
            type: ControlType.Image,
        },
        maxCount: 10,
    },
})

Countdown Timer

00:00:10

import * as React from "react"
import { addPropertyControls, ControlType, motion } from "framer"
import { useState, useEffect } from "react"

/**
 * Countdown Timer Component
 */
export default function CountdownTimer(props) {
    const {
        hours,
        minutes,
        seconds,
        showButton,
        buttonText,
        buttonBgColor,
        buttonTextColor,
        buttonBorderRadius,
        buttonBoxShadow,
        buttonHoverBgColor,
        buttonHoverTextColor,
        redirectLink,
        onTimerEnd,
    } = props

    // Convert initial time to total seconds
    const [timeLeft, setTimeLeft] = useState(
        hours * 3600 + minutes * 60 + seconds
    )

    // Timer logic
    useEffect(() => {
        if (timeLeft <= 0) {
            onTimerEnd?.()
            return
        }

        const interval = setInterval(() => {
            setTimeLeft((prev) => prev - 1)
        }, 1000)

        return () => clearInterval(interval)
    }, [timeLeft])

    // Format time into HH:MM:SS
    const formatTime = (totalSeconds) => {
        const hours = Math.floor(totalSeconds / 3600)
        const minutes = Math.floor((totalSeconds % 3600) / 60)
        const seconds = totalSeconds % 60

        return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(
            2,
            "0"
        )}:${String(seconds).padStart(2, "0")}`
    }

    // Handle button click
    const handleButtonClick = () => {
        if (redirectLink) {
            window.open(redirectLink, "_blank")
        }
    }

    return (
        <div style={containerStyle}>
            {timeLeft > 0 ? (
                <h1 style={timerStyle}>{formatTime(timeLeft)}</h1>
            ) : (
                showButton && (
                    <motion.button
                        style={{
                            ...buttonStyle,
                            backgroundColor: buttonBgColor,
                            color: buttonTextColor,
                            borderRadius: `${buttonBorderRadius}px`,
                            boxShadow: buttonBoxShadow,
                        }}
                        whileHover={{
                            backgroundColor: buttonHoverBgColor,
                            color: buttonHoverTextColor,
                        }}
                        onClick={handleButtonClick}
                    >
                        {buttonText}
                    </motion.button>
                )
            )}
        </div>
    )
}

// Default props
CountdownTimer.defaultProps = {
    hours: 0,
    minutes: 0,
    seconds: 10,
    showButton: true,
    buttonText: "Click Me",
    buttonBgColor: "#09F",
    buttonTextColor: "#FFF",
    buttonBorderRadius: 8,
    buttonBoxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
    buttonHoverBgColor: "#007ACC",
    buttonHoverTextColor: "#FFF",
    redirectLink: "https://www.example.com",
}

// Property Controls
addPropertyControls(CountdownTimer, {
    hours: {
        type: ControlType.Number,
        title: "Hours",
        min: 0,
        max: 24,
        defaultValue: 0,
    },
    minutes: {
        type: ControlType.Number,
        title: "Minutes",
        min: 0,
        max: 59,
        defaultValue: 0,
    },
    seconds: {
        type: ControlType.Number,
        title: "Seconds",
        min: 0,
        max: 59,
        defaultValue: 10,
    },
    showButton: {
        type: ControlType.Boolean,
        title: "Show Button",
        defaultValue: true,
    },
    buttonText: {
        type: ControlType.String,
        title: "Button Text",
        defaultValue: "Click Me",
        hidden: (props) => !props.showButton,
    },
    buttonBgColor: {
        type: ControlType.Color,
        title: "Button BG Color",
        defaultValue: "#09F",
        hidden: (props) => !props.showButton,
    },
    buttonTextColor: {
        type: ControlType.Color,
        title: "Button Text Color",
        defaultValue: "#FFF",
        hidden: (props) => !props.showButton,
    },
    buttonBorderRadius: {
        type: ControlType.Number,
        title: "Button Radius",
        min: 0,
        max: 100,
        defaultValue: 8,
        unit: "px",
        hidden: (props) => !props.showButton,
    },
    buttonBoxShadow: {
        type: ControlType.String,
        title: "Button Shadow",
        defaultValue: "0px 4px 6px rgba(0, 0, 0, 0.1)",
        hidden: (props) => !props.showButton,
    },
    buttonHoverBgColor: {
        type: ControlType.Color,
        title: "Hover BG Color",
        defaultValue: "#007ACC",
        hidden: (props) => !props.showButton,
    },
    buttonHoverTextColor: {
        type: ControlType.Color,
        title: "Hover Text Color",
        defaultValue: "#FFF",
        hidden: (props) => !props.showButton,
    },
    redirectLink: {
        type: ControlType.String,
        title: "Redirect Link",
        defaultValue: "https://www.example.com",
        hidden: (props) => !props.showButton,
    },
})

// Styles
const containerStyle = {
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    height: "100%",
    width: "100%",
}

const timerStyle = {
    fontSize: "48px",
    fontWeight: "bold",
    color: "#333",
}

const buttonStyle = {
    padding: "12px 24px",
    border: "none",
    cursor: "pointer",
    fontSize: "16px",
    fontWeight: "bold",
    transition: "background-color 0.3s, color 0.3s",
}

Circling Elements

Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
import * as React from "react"
import { Frame, addPropertyControls, ControlType } from "framer"

type Props = {
    radius: number
    duration: number
    direction: "clockwise" | "counterclockwise"
    imageCount: number
    imageSize: number
    speedUpOnHover: boolean
    // Groupe d'images sous forme de tableau de chaînes (URL)
    images: string[]
}

export function CirclingPictures(props: Props) {
    const {
        radius,
        duration,
        direction,
        imageCount,
        imageSize,
        speedUpOnHover,
        images,
    } = props

    // Calcul du container pour un "fit content" parfait : diamètre du cercle + taille d'une image.
    const containerSize = 2 * radius + imageSize

    // Référence pour le container qui contient les images.
    const containerRef = React.useRef<HTMLDivElement>(null)
    // Stockage de l’offset de rotation (mis à jour en interne sans provoquer de re-render).
    const offsetRef = React.useRef(0)
    // Référence pour le temps de la dernière frame.
    const lastTimeRef = React.useRef<number | null>(null)
    // Multiplicateur dynamique (calculé selon la position du curseur).
    const dynamicMultiplierRef = React.useRef(1)
    // Indique si la souris est sur le composant.
    const isHoveredRef = React.useRef(false)

    // Gestion des événements de la souris pour ajuster le multiplicateur dynamique.
    const handleMouseMove = (
        e: React.MouseEvent<HTMLDivElement, MouseEvent>
    ) => {
        if (!speedUpOnHover) return
        const rect = e.currentTarget.getBoundingClientRect()
        const center = rect.width / 2
        const x = e.clientX - rect.left
        const y = e.clientY - rect.top
        const dx = x - center
        const dy = y - center
        const distance = Math.sqrt(dx * dx + dy * dy)
        const normalized = Math.min(distance / center, 1)
        // Le multiplicateur dynamique varie de 3 (au centre) à 1 (au bord)
        dynamicMultiplierRef.current = 1 + (1 - normalized) * 2
    }

    const handleMouseEnter = () => {
        if (!speedUpOnHover) return
        isHoveredRef.current = true
    }

    const handleMouseLeave = () => {
        if (!speedUpOnHover) return
        dynamicMultiplierRef.current = 1
        isHoveredRef.current = false
    }

    // Boucle d'animation qui met à jour l'offset et les positions des images de façon impérative.
    React.useEffect(() => {
        let animationFrameId: number

        const update = (time: number) => {
            if (lastTimeRef.current !== null) {
                const deltaTime = (time - lastTimeRef.current) / 1000 // en secondes
                const effectiveMultiplier =
                    speedUpOnHover && isHoveredRef.current
                        ? dynamicMultiplierRef.current * 0.75
                        : 1
                // Calcul pour une rotation complète (2π) en "duration" secondes.
                const increment =
                    (2 * Math.PI * deltaTime * effectiveMultiplier) / duration
                const directionFactor = direction === "clockwise" ? 1 : -1
                offsetRef.current += directionFactor * increment

                // Mise à jour impérative des positions des enfants.
                if (containerRef.current) {
                    const children = containerRef.current.children
                    const count = children.length
                    const angleStep = count > 0 ? (2 * Math.PI) / count : 0
                    for (let i = 0; i < count; i++) {
                        const angle = i * angleStep + offsetRef.current
                        const x = radius * Math.cos(angle)
                        const y = radius * Math.sin(angle)
                        const child = children[i] as HTMLElement
                        child.style.left = `calc(50% + ${x}px)`
                        child.style.top = `calc(50% + ${y}px)`
                    }
                }
            }
            lastTimeRef.current = time
            animationFrameId = requestAnimationFrame(update)
        }
        animationFrameId = requestAnimationFrame(update)
        return () => cancelAnimationFrame(animationFrameId)
    }, [duration, direction, radius, speedUpOnHover])

    return (
        <Frame
            width={containerSize}
            height={containerSize}
            background="none"
            style={{ position: "relative", overflow: "visible" }}
            onMouseMove={handleMouseMove}
            onMouseEnter={handleMouseEnter}
            onMouseLeave={handleMouseLeave}
        >
            <div
                ref={containerRef}
                style={{ position: "absolute", width: "100%", height: "100%" }}
            >
                {images.slice(0, imageCount).map((img, index) => (
                    <div
                        key={index}
                        style={{
                            position: "absolute",
                            transform: "translate(-50%, -50%)",
                            width: imageSize,
                            height: imageSize,
                        }}
                    >
                        <img
                            src={img}
                            alt={`Image ${index + 1}`}
                            style={{
                                width: "100%",
                                height: "100%",
                                objectFit: "cover",
                            }}
                        />
                    </div>
                ))}
            </div>
        </Frame>
    )
}

CirclingPictures.defaultProps = {
    radius: 120,
    duration: 10,
    direction: "clockwise",
    imageCount: 7,
    imageSize: 80,
    speedUpOnHover: true,
    images: [
        "https://via.placeholder.com/150/FF0000/FFFFFF?text=Image1",
        "https://via.placeholder.com/150/00FF00/FFFFFF?text=Image2",
        "https://via.placeholder.com/150/0000FF/FFFFFF?text=Image3",
        "https://via.placeholder.com/150/FFFF00/FFFFFF?text=Image4",
        "https://via.placeholder.com/150/FF00FF/FFFFFF?text=Image5",
    ],
}

addPropertyControls(CirclingPictures, {
    radius: {
        type: ControlType.Number,
        title: "Radius",
        min: 0,
        max: 300,
        step: 1,
    },
    duration: {
        type: ControlType.Number,
        title: "Duration (sec)",
        min: 1,
        max: 60,
        step: 1,
    },
    direction: {
        type: ControlType.Enum,
        title: "Direction",
        options: ["clockwise", "counterclockwise"],
    },
    imageCount: {
        type: ControlType.Number,
        title: "Image Count",
        min: 1,
        max: 10,
        step: 1,
    },
    imageSize: {
        type: ControlType.Number,
        title: "Image Size",
        min: 10,
        max: 200,
        step: 1,
    },
    speedUpOnHover: {
        type: ControlType.Boolean,
        title: "Speed up on hover",
    },
    images: {
        type: ControlType.Array,
        title: "Images",
        control: {
            type: ControlType.Image,
        },
        maxCount: 10,
    },
})

Countdown Timer

00:00:10

import * as React from "react"
import { addPropertyControls, ControlType, motion } from "framer"
import { useState, useEffect } from "react"

/**
 * Countdown Timer Component
 */
export default function CountdownTimer(props) {
    const {
        hours,
        minutes,
        seconds,
        showButton,
        buttonText,
        buttonBgColor,
        buttonTextColor,
        buttonBorderRadius,
        buttonBoxShadow,
        buttonHoverBgColor,
        buttonHoverTextColor,
        redirectLink,
        onTimerEnd,
    } = props

    // Convert initial time to total seconds
    const [timeLeft, setTimeLeft] = useState(
        hours * 3600 + minutes * 60 + seconds
    )

    // Timer logic
    useEffect(() => {
        if (timeLeft <= 0) {
            onTimerEnd?.()
            return
        }

        const interval = setInterval(() => {
            setTimeLeft((prev) => prev - 1)
        }, 1000)

        return () => clearInterval(interval)
    }, [timeLeft])

    // Format time into HH:MM:SS
    const formatTime = (totalSeconds) => {
        const hours = Math.floor(totalSeconds / 3600)
        const minutes = Math.floor((totalSeconds % 3600) / 60)
        const seconds = totalSeconds % 60

        return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(
            2,
            "0"
        )}:${String(seconds).padStart(2, "0")}`
    }

    // Handle button click
    const handleButtonClick = () => {
        if (redirectLink) {
            window.open(redirectLink, "_blank")
        }
    }

    return (
        <div style={containerStyle}>
            {timeLeft > 0 ? (
                <h1 style={timerStyle}>{formatTime(timeLeft)}</h1>
            ) : (
                showButton && (
                    <motion.button
                        style={{
                            ...buttonStyle,
                            backgroundColor: buttonBgColor,
                            color: buttonTextColor,
                            borderRadius: `${buttonBorderRadius}px`,
                            boxShadow: buttonBoxShadow,
                        }}
                        whileHover={{
                            backgroundColor: buttonHoverBgColor,
                            color: buttonHoverTextColor,
                        }}
                        onClick={handleButtonClick}
                    >
                        {buttonText}
                    </motion.button>
                )
            )}
        </div>
    )
}

// Default props
CountdownTimer.defaultProps = {
    hours: 0,
    minutes: 0,
    seconds: 10,
    showButton: true,
    buttonText: "Click Me",
    buttonBgColor: "#09F",
    buttonTextColor: "#FFF",
    buttonBorderRadius: 8,
    buttonBoxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
    buttonHoverBgColor: "#007ACC",
    buttonHoverTextColor: "#FFF",
    redirectLink: "https://www.example.com",
}

// Property Controls
addPropertyControls(CountdownTimer, {
    hours: {
        type: ControlType.Number,
        title: "Hours",
        min: 0,
        max: 24,
        defaultValue: 0,
    },
    minutes: {
        type: ControlType.Number,
        title: "Minutes",
        min: 0,
        max: 59,
        defaultValue: 0,
    },
    seconds: {
        type: ControlType.Number,
        title: "Seconds",
        min: 0,
        max: 59,
        defaultValue: 10,
    },
    showButton: {
        type: ControlType.Boolean,
        title: "Show Button",
        defaultValue: true,
    },
    buttonText: {
        type: ControlType.String,
        title: "Button Text",
        defaultValue: "Click Me",
        hidden: (props) => !props.showButton,
    },
    buttonBgColor: {
        type: ControlType.Color,
        title: "Button BG Color",
        defaultValue: "#09F",
        hidden: (props) => !props.showButton,
    },
    buttonTextColor: {
        type: ControlType.Color,
        title: "Button Text Color",
        defaultValue: "#FFF",
        hidden: (props) => !props.showButton,
    },
    buttonBorderRadius: {
        type: ControlType.Number,
        title: "Button Radius",
        min: 0,
        max: 100,
        defaultValue: 8,
        unit: "px",
        hidden: (props) => !props.showButton,
    },
    buttonBoxShadow: {
        type: ControlType.String,
        title: "Button Shadow",
        defaultValue: "0px 4px 6px rgba(0, 0, 0, 0.1)",
        hidden: (props) => !props.showButton,
    },
    buttonHoverBgColor: {
        type: ControlType.Color,
        title: "Hover BG Color",
        defaultValue: "#007ACC",
        hidden: (props) => !props.showButton,
    },
    buttonHoverTextColor: {
        type: ControlType.Color,
        title: "Hover Text Color",
        defaultValue: "#FFF",
        hidden: (props) => !props.showButton,
    },
    redirectLink: {
        type: ControlType.String,
        title: "Redirect Link",
        defaultValue: "https://www.example.com",
        hidden: (props) => !props.showButton,
    },
})

// Styles
const containerStyle = {
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    height: "100%",
    width: "100%",
}

const timerStyle = {
    fontSize: "48px",
    fontWeight: "bold",
    color: "#333",
}

const buttonStyle = {
    padding: "12px 24px",
    border: "none",
    cursor: "pointer",
    fontSize: "16px",
    fontWeight: "bold",
    transition: "background-color 0.3s, color 0.3s",
}

Circling Elements

Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
import * as React from "react"
import { Frame, addPropertyControls, ControlType } from "framer"

type Props = {
    radius: number
    duration: number
    direction: "clockwise" | "counterclockwise"
    imageCount: number
    imageSize: number
    speedUpOnHover: boolean
    // Groupe d'images sous forme de tableau de chaînes (URL)
    images: string[]
}

export function CirclingPictures(props: Props) {
    const {
        radius,
        duration,
        direction,
        imageCount,
        imageSize,
        speedUpOnHover,
        images,
    } = props

    // Calcul du container pour un "fit content" parfait : diamètre du cercle + taille d'une image.
    const containerSize = 2 * radius + imageSize

    // Référence pour le container qui contient les images.
    const containerRef = React.useRef<HTMLDivElement>(null)
    // Stockage de l’offset de rotation (mis à jour en interne sans provoquer de re-render).
    const offsetRef = React.useRef(0)
    // Référence pour le temps de la dernière frame.
    const lastTimeRef = React.useRef<number | null>(null)
    // Multiplicateur dynamique (calculé selon la position du curseur).
    const dynamicMultiplierRef = React.useRef(1)
    // Indique si la souris est sur le composant.
    const isHoveredRef = React.useRef(false)

    // Gestion des événements de la souris pour ajuster le multiplicateur dynamique.
    const handleMouseMove = (
        e: React.MouseEvent<HTMLDivElement, MouseEvent>
    ) => {
        if (!speedUpOnHover) return
        const rect = e.currentTarget.getBoundingClientRect()
        const center = rect.width / 2
        const x = e.clientX - rect.left
        const y = e.clientY - rect.top
        const dx = x - center
        const dy = y - center
        const distance = Math.sqrt(dx * dx + dy * dy)
        const normalized = Math.min(distance / center, 1)
        // Le multiplicateur dynamique varie de 3 (au centre) à 1 (au bord)
        dynamicMultiplierRef.current = 1 + (1 - normalized) * 2
    }

    const handleMouseEnter = () => {
        if (!speedUpOnHover) return
        isHoveredRef.current = true
    }

    const handleMouseLeave = () => {
        if (!speedUpOnHover) return
        dynamicMultiplierRef.current = 1
        isHoveredRef.current = false
    }

    // Boucle d'animation qui met à jour l'offset et les positions des images de façon impérative.
    React.useEffect(() => {
        let animationFrameId: number

        const update = (time: number) => {
            if (lastTimeRef.current !== null) {
                const deltaTime = (time - lastTimeRef.current) / 1000 // en secondes
                const effectiveMultiplier =
                    speedUpOnHover && isHoveredRef.current
                        ? dynamicMultiplierRef.current * 0.75
                        : 1
                // Calcul pour une rotation complète (2π) en "duration" secondes.
                const increment =
                    (2 * Math.PI * deltaTime * effectiveMultiplier) / duration
                const directionFactor = direction === "clockwise" ? 1 : -1
                offsetRef.current += directionFactor * increment

                // Mise à jour impérative des positions des enfants.
                if (containerRef.current) {
                    const children = containerRef.current.children
                    const count = children.length
                    const angleStep = count > 0 ? (2 * Math.PI) / count : 0
                    for (let i = 0; i < count; i++) {
                        const angle = i * angleStep + offsetRef.current
                        const x = radius * Math.cos(angle)
                        const y = radius * Math.sin(angle)
                        const child = children[i] as HTMLElement
                        child.style.left = `calc(50% + ${x}px)`
                        child.style.top = `calc(50% + ${y}px)`
                    }
                }
            }
            lastTimeRef.current = time
            animationFrameId = requestAnimationFrame(update)
        }
        animationFrameId = requestAnimationFrame(update)
        return () => cancelAnimationFrame(animationFrameId)
    }, [duration, direction, radius, speedUpOnHover])

    return (
        <Frame
            width={containerSize}
            height={containerSize}
            background="none"
            style={{ position: "relative", overflow: "visible" }}
            onMouseMove={handleMouseMove}
            onMouseEnter={handleMouseEnter}
            onMouseLeave={handleMouseLeave}
        >
            <div
                ref={containerRef}
                style={{ position: "absolute", width: "100%", height: "100%" }}
            >
                {images.slice(0, imageCount).map((img, index) => (
                    <div
                        key={index}
                        style={{
                            position: "absolute",
                            transform: "translate(-50%, -50%)",
                            width: imageSize,
                            height: imageSize,
                        }}
                    >
                        <img
                            src={img}
                            alt={`Image ${index + 1}`}
                            style={{
                                width: "100%",
                                height: "100%",
                                objectFit: "cover",
                            }}
                        />
                    </div>
                ))}
            </div>
        </Frame>
    )
}

CirclingPictures.defaultProps = {
    radius: 120,
    duration: 10,
    direction: "clockwise",
    imageCount: 7,
    imageSize: 80,
    speedUpOnHover: true,
    images: [
        "https://via.placeholder.com/150/FF0000/FFFFFF?text=Image1",
        "https://via.placeholder.com/150/00FF00/FFFFFF?text=Image2",
        "https://via.placeholder.com/150/0000FF/FFFFFF?text=Image3",
        "https://via.placeholder.com/150/FFFF00/FFFFFF?text=Image4",
        "https://via.placeholder.com/150/FF00FF/FFFFFF?text=Image5",
    ],
}

addPropertyControls(CirclingPictures, {
    radius: {
        type: ControlType.Number,
        title: "Radius",
        min: 0,
        max: 300,
        step: 1,
    },
    duration: {
        type: ControlType.Number,
        title: "Duration (sec)",
        min: 1,
        max: 60,
        step: 1,
    },
    direction: {
        type: ControlType.Enum,
        title: "Direction",
        options: ["clockwise", "counterclockwise"],
    },
    imageCount: {
        type: ControlType.Number,
        title: "Image Count",
        min: 1,
        max: 10,
        step: 1,
    },
    imageSize: {
        type: ControlType.Number,
        title: "Image Size",
        min: 10,
        max: 200,
        step: 1,
    },
    speedUpOnHover: {
        type: ControlType.Boolean,
        title: "Speed up on hover",
    },
    images: {
        type: ControlType.Array,
        title: "Images",
        control: {
            type: ControlType.Image,
        },
        maxCount: 10,
    },
})

Let's Connect!

Let's Connect!

Let's Connect!

© Copyright 2025. All rights Reserved.

© Copyright 2025. All rights Reserved.