AnimatedLogos.tsx127 lines · main
1import { AnimatePresence, motion } from 'framer-motion'
2import { Axiom, Datadog, Grafana, Last9, Otlp, Sentry } from 'icons'
3import { BracesIcon, Cloud, Server } from 'lucide-react'
4import { useEffect, useState } from 'react'
5import { cn } from 'ui'
6
7interface AnimatedLogosProps {
8 iconSize?: number
9 className?: string
10}
11
12export const AnimatedLogos = ({ iconSize = 36, className }: AnimatedLogosProps) => {
13 const [currIndex, setCurrIndex] = useState(0)
14 const timer = 2500
15
16 const centerWrapperSize = Math.round(iconSize * 2.67)
17 const sideWrapperSize = Math.round(iconSize * 2.22)
18 const containerW = Math.round(iconSize * 5.33)
19 const containerH = Math.round(iconSize * 3.56)
20 const sideOffset = Math.round(iconSize * 2.22)
21 const hiddenOffset = Math.round(iconSize * 3.33)
22
23 const logos = [
24 { id: 'webhook', name: 'Custom Endpoint', icon: <BracesIcon size={iconSize} /> },
25 { id: 'otlp', name: 'OTLP', icon: <Otlp fill="currentColor" size={iconSize} /> },
26 { id: 'datadog', name: 'Datadog', icon: <Datadog fill="currentColor" size={iconSize} /> },
27 { id: 'loki', name: 'Loki', icon: <Grafana fill="currentColor" size={iconSize} /> },
28 { id: 's3', name: 'Amazon S3', icon: <Cloud size={iconSize} /> },
29 { id: 'sentry', name: 'Sentry', icon: <Sentry fill="currentColor" size={iconSize} /> },
30 { id: 'axiom', name: 'Axiom', icon: <Axiom fill="currentColor" size={iconSize} /> },
31 { id: 'last9', name: 'Last9', icon: <Last9 fill="currentColor" size={iconSize} /> },
32 { id: 'syslog', name: 'Syslog', icon: <Server size={iconSize} /> },
33 ]
34
35 useEffect(() => {
36 const interval = setInterval(() => {
37 setCurrIndex((prev) => (prev + 1) % logos.length)
38 }, timer)
39 return () => clearInterval(interval)
40 }, [logos.length])
41
42 const getPreviousIndex = () => (currIndex - 1 + logos.length) % logos.length
43 const getNextIndex = () => (currIndex + 1) % logos.length
44
45 const getPosition = (index: number) => {
46 if (index === currIndex) return 'center'
47 if (index === getPreviousIndex()) return 'left'
48 if (index === getNextIndex()) return 'right'
49 return 'hidden'
50 }
51
52 const logoVariants = {
53 hidden: {
54 x: `calc(-50% + ${hiddenOffset}px)`,
55 y: '-50%',
56 scale: 0.6,
57 opacity: 0,
58 filter: 'blur(1px)',
59 },
60 right: {
61 x: `calc(-50% + ${sideOffset}px)`,
62 y: '-50%',
63 scale: 0.8,
64 opacity: 0.5,
65 zIndex: 2,
66 filter: 'blur(1px)',
67 },
68 center: {
69 x: '-50%',
70 y: '-50%',
71 scale: 1,
72 opacity: 1,
73 zIndex: 3,
74 filter: 'blur(0px)',
75 },
76 left: {
77 x: `calc(-50% - ${sideOffset}px)`,
78 y: '-50%',
79 scale: 0.8,
80 opacity: 0.5,
81 zIndex: 2,
82 filter: 'blur(1px)',
83 },
84 exit: {
85 x: `calc(-50% - ${hiddenOffset}px)`,
86 y: '-50%',
87 scale: 0.6,
88 opacity: 0,
89 filter: 'blur(1px)',
90 },
91 }
92
93 const visibleIndices = [getPreviousIndex(), currIndex, getNextIndex()]
94
95 return (
96 <div
97 className={cn('relative overflow-hidden', className ?? 'mx-auto mb-8')}
98 style={{ width: containerW, height: containerH }}
99 >
100 <AnimatePresence initial={false}>
101 {logos.map((logo, index) => {
102 if (!visibleIndices.includes(index)) return null
103
104 const position = getPosition(index)
105 const isCenter = index === currIndex
106 const wrapperSize = isCenter ? centerWrapperSize : sideWrapperSize
107
108 return (
109 <motion.div
110 key={logo.id}
111 className="absolute top-1/2 left-1/2 flex items-center justify-center rounded-lg"
112 style={{ width: wrapperSize, height: wrapperSize }}
113 variants={logoVariants}
114 initial="hidden"
115 animate={position}
116 exit="exit"
117 transition={{ duration: 0.5, ease: 'easeInOut' }}
118 >
119 <span>{logo.icon}</span>
120 </motion.div>
121 )
122 })}
123 </AnimatePresence>
124 <div className="absolute -inset-4 bg-linear-to-r from-background-surface-75 via-transparent to-background-surface-75 z-40" />
125 </div>
126 )
127}