# Facade UI > Animated React components and full page blocks for local business, real estate and agency websites. Built for Next.js, Tailwind v4 and shadcn. Install with the shadcn CLI or let an AI agent install them through the Facade MCP server. Install a component: `npx shadcn@latest add https://facadeui.com/r/.json` MCP server: `claude mcp add --transport http facade https://facadeui.com/api/mcp` All components use semantic Tailwind tokens (bg-background, text-foreground, bg-brand ...) and only depend on motion and lucide-react. ## Components - [Magnetic button](https://facadeui.com/components/magnetic-button.md): Button that pulls toward the cursor with spring physics while its label drifts slightly further for depth. - [Shine button](https://facadeui.com/components/shine-button.md): Pill button with a soft light sweep on hover, three variants, three sizes and an arrow that nudges forward. - [Gradient heading](https://facadeui.com/components/gradient-heading.md): Display heading with a slow moving gradient sweep through your brand color, with optional highlighted words. - [Text reveal](https://facadeui.com/components/text-reveal.md): Headline that reveals word by word with a blur, fade and gentle rise as it scrolls into view. - [Agent card](https://facadeui.com/components/agent-card.md): Realtor contact card with initials avatar, rating, license, languages and call, email and tour actions. - [Property card](https://facadeui.com/components/property-card.md): Listing card with status badge, saved-home heart, price, address and beds, baths and square footage. - [Spotlight card](https://facadeui.com/components/spotlight-card.md): Card with a soft radial spotlight and a glowing border that both follow the cursor. - [Tilt card](https://facadeui.com/components/tilt-card.md): 3D perspective card that tilts toward the pointer with a moving glare and a smooth spring return. (Pro) - [Animated counter](https://facadeui.com/components/animated-counter.md): Number that counts up when scrolled into view, with prefix, suffix, decimals and Intl formatting. - [Availability table](https://facadeui.com/components/availability-table.md): Sortable, filterable unit availability table for new developments that turns into cards on mobile. (Pro) - [Countdown](https://facadeui.com/components/countdown.md): Countdown to an open house or launch with rolling flip digits for days, hours, minutes and seconds. - [Key facts](https://facadeui.com/components/key-facts.md): Property key facts row with icons and numbers that count up when scrolled into view. - [Before / after slider](https://facadeui.com/components/before-after-slider.md): Draggable, keyboard accessible comparison slider for renovations, staging and remodel photos. (Pro) - [Hotspot image](https://facadeui.com/components/hotspot-image.md): Image or floor plan with pulsing hotspots that open animated detail cards for each room or feature. (Pro) - [Image gallery lightbox](https://facadeui.com/components/image-gallery-lightbox.md): Listing photo grid that opens a shared layout lightbox with arrows, keyboard control and thumbnails. (Pro) - [Booking slots](https://facadeui.com/components/booking-slots.md): Appointment picker with a 14 day date strip, grouped time slots, summary and confirm callback. (Pro) - [Listing filters](https://facadeui.com/components/listing-filters.md): Home search filters with location search, type chips, beds and baths, dual price slider and result count. - [Mortgage calculator](https://facadeui.com/components/mortgage-calculator.md): Monthly payment calculator with down payment slider, loan term toggle and an animated donut breakdown. (Pro) - [Multi-step lead form](https://facadeui.com/components/multi-step-lead-form.md): Three step quote request with option cards, validation, progress bar and direction-aware transitions. (Pro) - [Price toggle](https://facadeui.com/components/price-toggle.md): Monthly or yearly segmented switch with a sliding thumb, plus an animated price that rolls its digits. - [Sliding tabs](https://facadeui.com/components/sliding-tabs.md): Accessible tabs with a spring animated sliding pill, arrow key navigation and a soft content crossfade. - [Sticky contact bar](https://facadeui.com/components/sticky-contact-bar.md): Mobile bottom bar with Call, Text, Directions and Book that hides on scroll down and returns on scroll up. - [Announcement bar](https://facadeui.com/components/announcement-bar.md): Slim top bar that rotates promo messages with a link and collapses smoothly when dismissed. - [Cookie consent](https://facadeui.com/components/cookie-consent.md): Floating consent card with Accept all, Reject and customizable Analytics and Marketing toggles. - [Review marquee](https://facadeui.com/components/review-marquee.md): Two rows of customer review cards scrolling in opposite directions with soft faded edges. (Pro) - [Star rating](https://facadeui.com/components/star-rating.md): Animated fractional star rating with review count, plus an accessible interactive input mode. - [Urgency badge](https://facadeui.com/components/urgency-badge.md): Scarcity badge with a pulsing live dot and an optional percent sold bar that animates into view. - [Aurora background](https://facadeui.com/components/aurora-background.md): Slowly drifting aurora of blurred brand colored blobs with subtle film grain, for heroes and CTAs. (Pro) - [Marquee](https://facadeui.com/components/marquee.md): Seamless infinite marquee for logos or testimonials with faded edges, direction, speed and vertical mode. - [Reveal](https://facadeui.com/components/reveal.md): Scroll reveal wrapper with fade, rise, blur and scale presets plus a RevealGroup for staggered children. - [Smooth accordion](https://facadeui.com/components/smooth-accordion.md): Accessible FAQ accordion with smooth height animation, a plus to minus icon morph and single or multiple mode. - [Location card](https://facadeui.com/components/location-card.md): Stylized SVG map card with a pulsing pin, address, hours, phone and a Google Maps directions link. - [Open now badge](https://facadeui.com/components/open-now-badge.md): Live open or closed pill computed from weekly hours in the business's own time zone. - [Opening hours](https://facadeui.com/components/opening-hours.md): Weekly hours list with today highlighted, live open status, split shifts and closed days. - [Service menu](https://facadeui.com/components/service-menu.md): Tabbed price list for salons, barbers and spas with durations, Popular tags and animated transitions. (Pro) ## Blocks - [Service area](https://facadeui.com/components/service-area.md): Interactive zone map with numbered service areas, city and ZIP lists, and an animated ZIP code checker. (Pro) - [New development showcase](https://facadeui.com/components/new-development-showcase.md): Interactive building elevation with clickable, status-coded units, a detail panel and availability counters. (Pro) - [Property listing grid](https://facadeui.com/components/property-listing-grid.md): Filterable, sortable listings grid with animated re-layout, saved homes and load more pagination. (Pro) - [Hero, agency](https://facadeui.com/components/hero-agency.md): Studio hero with oversized display type, animated grid, availability CTA and a client wordmark marquee. - [Hero, local business](https://facadeui.com/components/hero-local-business.md): Home services hero with live rating, rotating service words and floating appointment, review and ETA cards. - [Hero, real estate](https://facadeui.com/components/hero-real-estate.md): Luxury brokerage hero with ken burns architecture, glass Buy, Rent, Sell search panel and animated stats. (Pro) - [FAQ section](https://facadeui.com/components/faq-section.md): Two-column FAQ with an animated accordion, contact prompt and FAQPage JSON-LD for rich results. - [Features bento](https://facadeui.com/components/features-bento.md): Six cell bento grid with live micro animations: review counter, map pins, inbox stack, chart, toggle, booking. (Pro) - [Footer, big](https://facadeui.com/components/footer-big.md): Large local business footer with animated newsletter, link columns, NAP, hours, socials and a giant wordmark. - [Logo cloud](https://facadeui.com/components/logo-cloud.md): Trusted by wordmark grid with hover reveal of client outcomes and an optional two row marquee mode. - [Process steps](https://facadeui.com/components/process-steps.md): How it works section with a connecting line that draws on scroll and step cards that activate as you pass. - [Stats band](https://facadeui.com/components/stats-band.md): Band of four big animated numbers with labels, trend chips and divider lines that draw in on view. - [Team grid](https://facadeui.com/components/team-grid.md): Team members grid with gradient initials avatars, bio reveal on hover, social links and a hiring card. - [Testimonials wall](https://facadeui.com/components/testimonials-wall.md): Masonry wall of Google and Yelp style reviews with a featured quote and aggregate rating header. (Pro) - [Contact section](https://facadeui.com/components/contact-section.md): Contact form with floating labels and inline validation, plus NAP details, live open-now hours and a map card. (Pro) - [CTA section](https://facadeui.com/components/cta-section.md): High contrast closing CTA panel with brand glow, two actions, reassurance line and an animated beam border. - [Home valuation funnel](https://facadeui.com/components/valuation-funnel.md): Multi-step "What's my home worth?" lead funnel with direction-aware steps and an animated estimate range. (Pro) - [Pricing section](https://facadeui.com/components/pricing-section.md): Three-tier pricing with an animated monthly/yearly toggle, glowing featured plan and feature lists. (Pro) ## Docs - [Installation](https://facadeui.com/docs/installation) - [Use with AI agents](https://facadeui.com/docs/ai) - [Pricing](https://facadeui.com/pricing) # Free component sources ## Magnetic button (magnetic-button) Button that pulls toward the cursor with spring physics while its label drifts slightly further for depth. ```tsx "use client"; import * as React from "react"; import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react"; import { cn } from "@/lib/utils"; export type MagneticButtonVariant = "brand" | "solid" | "outline"; export interface MagneticButtonProps extends Omit< React.ButtonHTMLAttributes, "onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart" | "onAnimationEnd" > { /** How strongly the shell follows the cursor, 0 to 1. Defaults to 0.35. */ strength?: number; /** Extra multiplier applied to the inner label. Defaults to 1.6. */ labelStrength?: number; /** Distance in px outside the button where the pull starts. Defaults to 40. */ radius?: number; variant?: MagneticButtonVariant; /** Pass `href` to render an `` instead of a ` ); } export default ShineButton; ``` ## Gradient heading (gradient-heading) Display heading with a slow moving gradient sweep through your brand color, with optional highlighted words. ```tsx "use client"; import * as React from "react"; import { motion, useReducedMotion } from "motion/react"; import { cn } from "@/lib/utils"; export interface GradientHeadingProps { /** Heading copy. */ children?: string; /** Element to render. Defaults to "h1". */ as?: "h1" | "h2" | "h3"; /** * Words (or phrases) that get the vivid brand gradient. When omitted the * whole heading sweeps through brand and foreground. */ highlight?: string[]; /** Seconds for one full sweep. Defaults to 9. */ duration?: number; className?: string; /** Class applied to highlighted segments. */ highlightClassName?: string; } const vivid = "linear-gradient(100deg, var(--foreground) 0%, var(--brand) 22%, color-mix(in oklch, var(--brand) 55%, var(--foreground)) 40%, var(--foreground) 55%, var(--brand) 78%, var(--foreground) 100%)"; const subtle = "linear-gradient(100deg, var(--foreground) 0%, color-mix(in oklch, var(--foreground) 62%, transparent) 25%, var(--foreground) 50%, color-mix(in oklch, var(--foreground) 62%, transparent) 75%, var(--foreground) 100%)"; function escapeRegExp(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function Sweep({ gradient, duration, reduce, className, children, }: { gradient: string; duration: number; reduce: boolean; className?: string; children: React.ReactNode; }) { return ( {children} ); } export function GradientHeading({ children = "Websites that make the phone ring.", as = "h1", highlight, duration = 9, className, highlightClassName, }: GradientHeadingProps) { const reduce = useReducedMotion() ?? false; const Tag = as; const parts = React.useMemo(() => { if (!highlight || highlight.length === 0) return null; const re = new RegExp(`(${highlight.map(escapeRegExp).join("|")})`, "g"); return children.split(re).filter((p) => p.length > 0); }, [children, highlight]); return ( {parts ? ( parts.map((part, i) => highlight?.includes(part) ? ( {part} ) : ( {part} ), ) ) : ( {children} )} ); } export default GradientHeading; ``` ## Text reveal (text-reveal) Headline that reveals word by word with a blur, fade and gentle rise as it scrolls into view. ```tsx "use client"; import * as React from "react"; import { motion, useInView, useReducedMotion } from "motion/react"; import type { Variants } from "motion/react"; import { cn } from "@/lib/utils"; export interface TextRevealProps { /** The text to reveal, split on whitespace. */ text: string; /** Element to render. Defaults to "h2". */ as?: "h1" | "h2" | "h3" | "p"; /** Delay in seconds before the first word. Defaults to 0. */ delay?: number; /** Seconds between each word. Defaults to 0.06. */ stagger?: number; /** Reveal only once. Defaults to true. */ once?: boolean; className?: string; /** Class applied to every word span. */ wordClassName?: string; } const word: Variants = { hidden: { opacity: 0, y: "0.45em", filter: "blur(10px)" }, visible: { opacity: 1, y: "0em", filter: "blur(0px)", transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1] }, }, }; const wordReduced: Variants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 0.3 } }, }; export function TextReveal({ text, as = "h2", delay = 0, stagger = 0.06, once = true, className, wordClassName, }: TextRevealProps) { const ref = React.useRef(null); const inView = useInView(ref, { once, amount: 0.4 }); const reduce = useReducedMotion(); const words = text.split(/\s+/).filter(Boolean); const Tag = as; return ( } className={cn(className)}> {text} {words.map((w, i) => ( {w} {i < words.length - 1 ? " " : null} ))} ); } export default TextReveal; ``` ## Agent card (agent-card) Realtor contact card with initials avatar, rating, license, languages and call, email and tour actions. ```tsx "use client"; import * as React from "react"; import { motion, useReducedMotion } from "motion/react"; import { Calendar, Mail, Phone, Star } from "lucide-react"; import { cn } from "@/lib/utils"; export interface AgentCardProps { name?: string; title?: string; brokerage?: string; license?: string; /** Optional headshot URL. Falls back to an initials avatar. */ photoSrc?: string; rating?: number; reviewCount?: number; languages?: string[]; phone?: string; email?: string; /** Link or handler for the schedule tour action. */ scheduleHref?: string; onSchedule?: () => void; /** Optional stats shown under the header, e.g. homes sold. */ stats?: Array<{ label: string; value: string }>; /** Show the online status dot on the avatar. */ available?: boolean; className?: string; } function initialsOf(name: string) { return name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((p) => p[0]?.toUpperCase() ?? "") .join(""); } function Rating({ value, count }: { value: number; count?: number }) { const rounded = Math.round(value * 2) / 2; return (
{value.toFixed(1)} {count !== undefined && ({count})}
); } export function AgentCard({ name = "Danielle Ortiz", title = "Realtor", brokerage = "Summit Peak Realty", license = "CO FA.100082417", photoSrc, rating = 4.9, reviewCount = 128, languages = ["English", "Spanish"], phone = "(303) 555-0148", email = "danielle@summitpeakrealty.com", scheduleHref = "#schedule", onSchedule, stats, available = true, className, }: AgentCardProps) { const reduce = useReducedMotion(); const tel = `tel:${phone.replace(/[^0-9+]/g, "")}`; const btnBase = "inline-flex h-10 items-center justify-center gap-2 rounded-xl px-3 text-sm font-medium transition-[background-color,border-color,color,opacity] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card"; return (
{photoSrc ? ( // eslint-disable-next-line @next/next/no-img-element {name} ) : ( )}
{available && ( )}

{name}

{title} ยท {brokerage}

{stats && stats.length > 0 && (
{stats.slice(0, 3).map((s) => (
{s.label}
{s.value}
))}
)}
{languages.map((l) => ( {l} ))} Lic. {license}
{onSchedule ? ( ) : ( )}
); } export default AgentCard; ``` ## Property card (property-card) Listing card with status badge, saved-home heart, price, address and beds, baths and square footage. ```tsx "use client"; import * as React from "react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { Bath, BedDouble, Heart, MapPin, Ruler } from "lucide-react"; import { cn } from "@/lib/utils"; export type PropertyStatus = "for-sale" | "pending" | "new"; export interface PropertyCardProps { /** Optional photo URL. When omitted, an illustrated architectural placeholder renders. */ src?: string; alt?: string; status?: PropertyStatus; price?: number; /** Optional suffix such as "/mo" for rentals. */ priceSuffix?: string; address?: string; city?: string; beds?: number; baths?: number; sqft?: number; /** Placeholder composition when no src is set. */ placeholder?: "modern" | "bungalow" | "tower"; href?: string; defaultFavorite?: boolean; onFavoriteChange?: (favorite: boolean) => void; className?: string; } const statusCopy: Record = { "for-sale": { label: "For sale", className: "bg-background/90 text-foreground", dot: "bg-success", }, pending: { label: "Pending", className: "bg-background/90 text-foreground", dot: "bg-warning", }, new: { label: "New", className: "bg-brand text-brand-foreground", dot: "bg-brand-foreground", }, }; const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }); function PropertyPlaceholder({ variant }: { variant: "modern" | "bungalow" | "tower" }) { const id = React.useId().replace(/[^a-zA-Z0-9_-]/g, ""); return ( ); } export function PropertyCard({ src, alt = "Property photo", status = "for-sale", price = 685000, priceSuffix, address = "2418 Travis Heights Blvd", city = "Austin, TX 78704", beds = 3, baths = 2.5, sqft = 2140, placeholder = "modern", href, defaultFavorite = false, onFavoriteChange, className, }: PropertyCardProps) { const reduce = useReducedMotion(); const [favorite, setFavorite] = React.useState(defaultFavorite); const s = statusCopy[status]; const toggle = () => { const next = !favorite; setFavorite(next); onFavoriteChange?.(next); }; const Title = href ? "a" : "span"; return (
{src ? ( // eslint-disable-next-line @next/next/no-img-element {alt} ) : ( )}
{s.label}
{usd.format(price)} {priceSuffix && {priceSuffix}}
{address}

); } export default PropertyCard; ``` ## Spotlight card (spotlight-card) Card with a soft radial spotlight and a glowing border that both follow the cursor. ```tsx "use client"; import * as React from "react"; import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from "motion/react"; import { cn } from "@/lib/utils"; export interface SpotlightCardProps extends React.HTMLAttributes { /** Spotlight radius in px. Defaults to 360. */ size?: number; /** Border glow radius in px. Defaults to 220. */ borderSize?: number; /** Spotlight strength as a percentage of brand color. Defaults to 14. */ intensity?: number; className?: string; children?: React.ReactNode; } export function SpotlightCard({ size = 360, borderSize = 220, intensity = 14, className, children, onPointerMove, onPointerEnter, onPointerLeave, ...rest }: SpotlightCardProps) { const reduce = useReducedMotion(); const x = useMotionValue(-1000); const y = useMotionValue(-1000); const [active, setActive] = React.useState(false); const spot = useMotionTemplate`radial-gradient(${size}px circle at ${x}px ${y}px, color-mix(in oklch, var(--brand) ${intensity}%, transparent), transparent 70%)`; const ring = useMotionTemplate`radial-gradient(${borderSize}px circle at ${x}px ${y}px, color-mix(in oklch, var(--brand) 85%, transparent), transparent 65%)`; return (
{ const rect = e.currentTarget.getBoundingClientRect(); x.set(e.clientX - rect.left); y.set(e.clientY - rect.top); onPointerMove?.(e); }} onPointerEnter={(e) => { setActive(true); onPointerEnter?.(e); }} onPointerLeave={(e) => { setActive(false); onPointerLeave?.(e); }} className={cn( "group/spotlight relative isolate overflow-hidden rounded-2xl border border-border bg-card text-card-foreground", "shadow-[0_1px_2px_0_color-mix(in_oklch,var(--foreground)_5%,transparent)]", className, )} > {!reduce ? ( <> {/* Border glow: a masked layer that only paints the 1px border ring. */} ) : null} {children}
); } export default SpotlightCard; ``` ## Animated counter (animated-counter) Number that counts up when scrolled into view, with prefix, suffix, decimals and Intl formatting. ```tsx "use client"; import * as React from "react"; import { animate, useInView, useReducedMotion } from "motion/react"; import { cn } from "@/lib/utils"; export interface AnimatedCounterProps extends Omit, "children"> { /** Target value. */ value: number; /** Starting value. Defaults to 0. */ from?: number; /** Text before the number, e.g. "$". */ prefix?: string; /** Text after the number, e.g. "+", "M", "%". */ suffix?: string; /** Fixed number of decimals. Defaults to 0. */ decimals?: number; /** BCP 47 locale for Intl.NumberFormat. Defaults to "en-US". */ locale?: string; /** Extra Intl.NumberFormat options (merged after decimals). */ formatOptions?: Intl.NumberFormatOptions; /** Animation duration in seconds. Defaults to 1.8. */ duration?: number; /** Delay in seconds once in view. Defaults to 0. */ delay?: number; /** Animate only the first time it enters the viewport. Defaults to true. */ once?: boolean; className?: string; } export function AnimatedCounter({ value, from = 0, prefix = "", suffix = "", decimals = 0, locale = "en-US", formatOptions, duration = 1.8, delay = 0, once = true, className, "aria-label": ariaLabel, ...rest }: AnimatedCounterProps) { const ref = React.useRef(null); const inView = useInView(ref, { once, margin: "0px 0px -10% 0px" }); const reduce = useReducedMotion(); // Compare `formatOptions` by value so an inline object does not restart the animation on every parent render. const formatOptionsKey = JSON.stringify(formatOptions ?? null); const formatter = React.useMemo(() => { const extra = JSON.parse(formatOptionsKey) as Intl.NumberFormatOptions | null; return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, ...extra, }); }, [locale, decimals, formatOptionsKey]); const format = React.useCallback((n: number) => `${prefix}${formatter.format(n)}${suffix}`, [formatter, prefix, suffix]); React.useEffect(() => { const node = ref.current; if (!node) return; if (!inView) { if (!once) node.textContent = format(from); return; } if (reduce) { node.textContent = format(value); return; } const controls = animate(from, value, { duration, delay, ease: [0.16, 1, 0.3, 1], onUpdate: (v) => { node.textContent = format(v); }, }); return () => controls.stop(); }, [inView, from, value, duration, delay, reduce, once, format]); return ( {/* Screen readers get the final value; the animated text is decorative. */} {ariaLabel ?? format(value)} {format(from)} ); } export default AnimatedCounter; ``` ## Countdown (countdown) Countdown to an open house or launch with rolling flip digits for days, hours, minutes and seconds. ```tsx "use client"; import * as React from "react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { cn } from "@/lib/utils"; export interface CountdownProps { /** Target date. Accepts a Date, ISO string or timestamp. Include a UTC offset in strings ("2026-11-14T10:00:00-05:00") so the event time is the same for every visitor. */ target?: Date | string | number; /** Called once when the countdown reaches zero. */ onComplete?: () => void; /** Labels for each unit. */ labels?: { days: string; hours: string; minutes: string; seconds: string }; /** Hide the seconds column. */ hideSeconds?: boolean; /** Rendered when time is up. */ completeContent?: React.ReactNode; size?: "sm" | "md" | "lg"; className?: string; } type Parts = { days: number; hours: number; minutes: number; seconds: number }; function getParts(ms: number): Parts { const total = Math.max(0, Math.floor(ms / 1000)); return { days: Math.floor(total / 86400), hours: Math.floor((total % 86400) / 3600), minutes: Math.floor((total % 3600) / 60), seconds: total % 60, }; } // md and lg shrink below the sm breakpoint so four units plus separators fit a 360px viewport. const sizes = { sm: { box: "h-10 w-7 text-xl rounded-md", gap: "gap-1", label: "text-[10px]", sep: "text-lg", sepOffset: "mt-1" }, md: { box: "h-10 w-7 text-xl rounded-md sm:h-14 sm:w-10 sm:text-3xl sm:rounded-lg", gap: "gap-1", label: "text-[11px]", sep: "text-lg sm:text-2xl", sepOffset: "mt-1 sm:mt-2.5" }, lg: { box: "h-11 w-8 text-2xl rounded-md sm:h-20 sm:w-14 sm:text-5xl sm:rounded-xl", gap: "gap-1 sm:gap-1.5", label: "text-[11px] sm:text-xs", sep: "text-xl sm:text-4xl", sepOffset: "mt-1.5 sm:mt-4" }, }; function Digit({ value, size }: { value: string; size: keyof typeof sizes }) { const reduce = useReducedMotion(); return ( {/* split line for the flip-card look */}