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







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







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







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, }, })