Expanding Location Map
Expanding Location Map é um componente de media para React e Tailwind CSS, escrito para a Uncode e teu para usares. Copia e cola no teu projeto.
Uncode
media
O que é
Expanding Location Map é um componente de media para React e Tailwind CSS, escrito para a Uncode e teu para usares. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i framer-motion
Licença
Este componente foi escrito para este catálogo. É teu para usares em trabalho de cliente e em produtos que vendas, nos termos de uso.
Expanding Location Map
"use client"
import type React from "react"
import { useState, useRef } from "react"
import { motion, AnimatePresence, useMotionValue, useTransform, useSpring } from "framer-motion"
interface LocationMapProps {
location?: string
coordinates?: string
className?: string
}
export function LocationMap({
location = "San Francisco, CA",
coordinates = "37.7749° N, 122.4194° W",
className,
}: LocationMapProps) {
const [isHovered, setIsHovered] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const mouseX = useMotionValue(0)
const mouseY = useMotionValue(0)
const rotateX = useTransform(mouseY, [-50, 50], [8, -8])
const rotateY = useTransform(mouseX, [-50, 50], [-8, 8])
const springRotateX = useSpring(rotateX, { stiffness: 300, damping: 30 })
const springRotateY = useSpring(rotateY, { stiffness: 300, damping: 30 })
const handleMouseMove = (e: React.MouseEvent) => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
mouseX.set(e.clientX - centerX)
mouseY.set(e.clientY - centerY)
}
const handleMouseLeave = () => {
mouseX.set(0)
mouseY.set(0)
setIsHovered(false)
}
const handleClick = () => {
setIsExpanded(!isExpanded)
}
return (
<motion.div
ref={containerRef}
className={`relative cursor-pointer select-none ${className}`}
style={{
perspective: 1000,
}}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
>
<motion.div
className="relative overflow-hidden rounded-2xl bg-background border border-border"
style={{
rotateX: springRotateX,
rotateY: springRotateY,
transformStyle: "preserve-3d",
}}
animate={{
width: isExpanded ? 360 : 240,
height: isExpanded ? 280 : 140,
}}
transition={{
type: "spring",
stiffness: 400,
damping: 35,
}}
>
{/* Subtle gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-muted/20 via-transparent to-muted/40" />
<AnimatePresence>
{isExpanded && (
<motion.div
className="absolute inset-0 pointer-events-none"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<div className="absolute inset-0 bg-muted" />
<svg className="absolute inset-0 w-full h-full" preserveAspectRatio="none">
{/* Main roads - using foreground with opacity */}
<motion.line
x1="0%"
y1="35%"
x2="100%"
y2="35%"
className="stroke-foreground/25"
strokeWidth="4"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
/>
<motion.line
x1="0%"
y1="65%"
x2="100%"
y2="65%"
className="stroke-foreground/25"
strokeWidth="4"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.8, delay: 0.3 }}
/>
{/* Vertical main roads */}
<motion.line
x1="30%"
y1="0%"
x2="30%"
y2="100%"
className="stroke-foreground/20"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.6, delay: 0.4 }}
/>
<motion.line
x1="70%"
y1="0%"
x2="70%"
y2="100%"
className="stroke-foreground/20"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.6, delay: 0.5 }}
/>
{/* Secondary streets */}
{[20, 50, 80].map((y, i) => (
<motion.line
key={`h-${i}`}
x1="0%"
y1={`${y}%`}
x2="100%"
y2={`${y}%`}
className="stroke-foreground/10"
strokeWidth="1.5"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.5, delay: 0.6 + i * 0.1 }}
/>
))}
{[15, 45, 55, 85].map((x, i) => (
<motion.line
key={`v-${i}`}
x1={`${x}%`}
y1="0%"
x2={`${x}%`}
y2="100%"
className="stroke-foreground/10"
strokeWidth="1.5"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.5, delay: 0.7 + i * 0.1 }}
/>
))}
</svg>
{/* Buildings - using muted-foreground */}
<motion.div
className="absolute top-[40%] left-[10%] w-[15%] h-[20%] rounded-sm bg-muted-foreground/30 border border-muted-foreground/20"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.5 }}
/>
<motion.div
className="absolute top-[15%] left-[35%] w-[12%] h-[15%] rounded-sm bg-muted-foreground/25 border border-muted-foreground/15"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.6 }}
/>
<motion.div
className="absolute top-[70%] left-[75%] w-[18%] h-[18%] rounded-sm bg-muted-foreground/28 border border-muted-foreground/18"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.7 }}
/>
<motion.div
className="absolute top-[20%] right-[10%] w-[10%] h-[25%] rounded-sm bg-muted-foreground/22 border border-muted-foreground/15"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.55 }}
/>
<motion.div
className="absolute top-[55%] left-[5%] w-[8%] h-[12%] rounded-sm bg-muted-foreground/20 border border-muted-foreground/12"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.65 }}
/>
<motion.div
className="absolute top-[8%] left-[75%] w-[14%] h-[10%] rounded-sm bg-muted-foreground/22 border border-muted-foreground/15"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.75 }}
/>
<motion.div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
initial={{ scale: 0, y: -20 }}
animate={{ scale: 1, y: 0 }}
transition={{ type: "spring", stiffness: 400, damping: 20, delay: 0.3 }}
>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
className="drop-shadow-lg"
style={{ filter: "drop-shadow(0 0 10px rgba(52, 211, 153, 0.5))" }}
>
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" fill="#34D399" />
<circle cx="12" cy="9" r="2.5" className="fill-background" />
</svg>
</motion.div>
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent opacity-60" />
</motion.div>
)}
</AnimatePresence>
{/* Grid pattern - only show when collapsed */}
<motion.div
className="absolute inset-0 opacity-[0.03]"
animate={{ opacity: isExpanded ? 0 : 0.03 }}
transition={{ duration: 0.3 }}
>
<svg width="100%" height="100%" className="absolute inset-0">
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" className="stroke-foreground" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</motion.div>
{/* Content */}
<div className="relative z-10 h-full flex flex-col justify-between p-5">
{/* Top section */}
<div className="flex items-start justify-between">
<div className="relative">
<motion.div
className="relative"
animate={{
opacity: isExpanded ? 0 : 1,
}}
transition={{ duration: 0.3 }}
>
{/* Map Icon SVG */}
<motion.svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-emerald-400"
animate={{
filter: isHovered
? "drop-shadow(0 0 8px rgba(52, 211, 153, 0.6))"
: "drop-shadow(0 0 4px rgba(52, 211, 153, 0.3))",
}}
transition={{ duration: 0.3 }}
>
<polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21" />
<line x1="9" x2="9" y1="3" y2="18" />
<line x1="15" x2="15" y1="6" y2="21" />
</motion.svg>
</motion.div>
</div>
{/* Status indicator */}
<motion.div
className="flex items-center gap-1.5 px-2 py-1 rounded-full bg-foreground/5 backdrop-blur-sm"
animate={{
scale: isHovered ? 1.05 : 1,
backgroundColor: isHovered ? "hsl(var(--foreground) / 0.08)" : "hsl(var(--foreground) / 0.05)",
}}
transition={{ duration: 0.2 }}
>
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
<span className="text-[10px] font-medium text-muted-foreground tracking-wide uppercase">Live</span>
</motion.div>
</div>
{/* Bottom section */}
<div className="space-y-1">
<motion.h3
className="text-foreground font-medium text-sm tracking-tight"
animate={{
x: isHovered ? 4 : 0,
}}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
>
{location}
</motion.h3>
<AnimatePresence>
{isExpanded && (
<motion.p
className="text-muted-foreground text-xs font-mono"
initial={{ opacity: 0, y: -10, height: 0 }}
animate={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, y: -10, height: 0 }}
transition={{ duration: 0.25 }}
>
{coordinates}
</motion.p>
)}
</AnimatePresence>
{/* Animated underline */}
<motion.div
className="h-px bg-gradient-to-r from-emerald-500/50 via-emerald-400/30 to-transparent"
initial={{ scaleX: 0, originX: 0 }}
animate={{
scaleX: isHovered || isExpanded ? 1 : 0.3,
}}
transition={{ duration: 0.4, ease: "easeOut" }}
/>
</div>
</div>
</motion.div>
{/* Click hint */}
<motion.p
className="absolute -bottom-6 left-1/2 text-[10px] text-muted-foreground whitespace-nowrap"
style={{ x: "-50%" }}
initial={{ opacity: 0 }}
animate={{
opacity: isHovered && !isExpanded ? 1 : 0,
y: isHovered ? 0 : 4,
}}
transition={{ duration: 0.2 }}
>
Click to expand
</motion.p>
</motion.div>
)
}