"use client"
import React, {
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react"
// next
import Link from "next/link"
// shadcn
import { Button } from "@/components/ui/button"
// third-party
import { cn } from "cn"
import {
AnimatePresence,
motion,
useMotionValue,
useSpring,
} from "framer-motion"
// project-imports
import SectionHeader from "@/components/uiable/blocks/landing/components/SectionHeader"
import { getTemplateHref } from "@/data/templates"
// assets
import { IconArrowRight } from "@tabler/icons-react"
import { Eye, Maximize2, Move, Play } from "lucide-react"
// types
interface ShowcasePreview {
src: string
darkSrc?: string
alt: string
href: string
newTab?: boolean
}
interface ShowcaseRow {
id: string
title: string
description: string
href: string
newTab?: boolean
previews: ShowcasePreview[]
}
// ------------------------------ | CURSOR CONTEXT | ------------------------------ //
type ContextType = "view" | "play" | "zoom" | "drag" | null
interface CursorContextType {
setHoverType: (type: ContextType) => void
hoverType: ContextType
}
const CursorHoverContext = createContext<CursorContextType | null>(null)
interface CursorAreaProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
}
interface CursorTargetProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
type: ContextType
}
function useCursor() {
const context = useContext(CursorHoverContext)
if (!context) {
throw new Error("useCursor must be used within a CursorArea")
}
return context
}
export function CursorArea({ children, className, ...props }: CursorAreaProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [hoverType, setHoverType] = useState<ContextType>(null)
// Tracking position
const mouseX = useMotionValue(0)
const mouseY = useMotionValue(0)
// Springs
const springConfig = { damping: 28, stiffness: 350, mass: 0.5 }
const cursorX = useSpring(mouseX, springConfig)
const cursorY = useSpring(mouseY, springConfig)
const [isInside, setIsInside] = useState(false)
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
mouseX.set(e.clientX - rect.left)
mouseY.set(e.clientY - rect.top)
}
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
mouseX.jump(x)
mouseY.jump(y)
cursorX.jump(x)
cursorY.jump(y)
setIsInside(true)
}
// Target width for each hover state ensuring symmetric, unified padding
const getTargetWidth = (type: ContextType) => {
switch (type) {
case "view":
return 76
case "play":
return 74
case "zoom":
return 80
case "drag":
return 76
default:
return 12
}
}
// Get matching icon and styling based on active hover type
const renderCursorContent = () => {
switch (hoverType) {
case "view":
return (
<>
<Eye className="size-3.5" />
<span>View</span>
</>
)
case "play":
return (
<>
<Play className="size-3.5 fill-current" />
<span>Play</span>
</>
)
case "zoom":
return (
<>
<Maximize2 className="size-3.5" />
<span>Zoom</span>
</>
)
case "drag":
return (
<>
<Move className="size-3.5" />
<span>Drag</span>
</>
)
default:
return null
}
}
return (
<CursorHoverContext.Provider value={{ hoverType, setHoverType }}>
<div
ref={containerRef}
className={cn("relative cursor-none overflow-hidden", className)}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => {
setIsInside(false)
setHoverType(null)
}}
{...props}
>
{children}
{/* Custom Bounded Context Cursor */}
<AnimatePresence>
{isInside && (
<motion.div
className={cn(
"pointer-events-none absolute top-0 left-0 z-50 flex items-center justify-center overflow-hidden border font-medium shadow-lg transition-colors duration-150 select-none",
hoverType
? "border-border bg-background text-foreground"
: "border-primary bg-primary text-transparent"
)}
style={{
x: cursorX,
y: cursorY,
translateX: "-50%",
translateY: "-50%",
}}
initial={{ scale: 0, opacity: 0 }}
animate={{
scale: 1,
opacity: 1,
width: hoverType ? getTargetWidth(hoverType) : 12,
height: hoverType ? 32 : 12,
borderRadius: hoverType ? 16 : 6,
}}
exit={{ scale: 0, opacity: 0 }}
transition={{
scale: { duration: 0.12 },
opacity: { duration: 0.12 },
width: {
type: "spring",
stiffness: 450,
damping: 30,
mass: 0.5,
},
height: {
type: "spring",
stiffness: 450,
damping: 30,
mass: 0.5,
},
borderRadius: {
type: "spring",
stiffness: 450,
damping: 30,
mass: 0.5,
},
}}
>
{hoverType && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
className="flex items-center justify-center gap-1.5 px-3 text-[11px] whitespace-nowrap"
>
{renderCursorContent()}
</motion.div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</CursorHoverContext.Provider>
)
}
export function CursorTarget({
children,
type,
className,
...props
}: CursorTargetProps) {
const { setHoverType } = useCursor()
return (
<div
className={cn("relative z-10", className)}
onMouseEnter={() => setHoverType(type)}
onMouseLeave={() => setHoverType(null)}
{...props}
>
{children}
</div>
)
}
// ------------------------------ | CONSTANTS | ------------------------------ //
const ROTATE_INTERVAL_MS = 3000
const rows: ShowcaseRow[] = [
{
id: "landing-pages",
title: "Landing Pages",
description:
"Engaging landing pages designed to convert, tailored for any business or personal project.",
href: "/templates",
previews: [
{
src: "https://cdn.uiable.com/block/landing-portfolio-1.png",
alt: "portfolio landing page template",
href: getTemplateHref("portfolio-1"),
newTab: true,
},
{
src: "https://cdn.uiable.com/block/landing-finance-1.png",
alt: "Finance landing page template",
href: getTemplateHref("finance-1"),
newTab: true,
},
{
src: "https://cdn.uiable.com/block/landing-lms-2.png",
alt: "LMS landing page template",
href: getTemplateHref("lms-1"),
newTab: true,
},
],
},
{
id: "dashboards",
title: "Dashboard & Application",
description:
"Customizable dashboards and app templates for eCommerce, CRM, SaaS, logistics, and more.",
href: "/admin-dashboard",
newTab: true,
previews: [
{
src: "https://cdn.uiable.com/block/dashboard-lms.png",
darkSrc: "https://cdn.uiable.com/block/dashboard-lms-dark.png",
alt: "eCommerce admin dashboard",
href: "/admin-dashboard/orbitcn/preview/dashboard/lms",
newTab: true,
},
{
src: "https://cdn.uiable.com/block/dashboard-finance.png",
darkSrc: "https://cdn.uiable.com/block/dashboard-finance-dark.png",
alt: "Finance admin dashboard",
href: "/admin-dashboard/orbitcn/preview/dashboard/finance",
newTab: true,
},
{
src: "https://cdn.uiable.com/block/dashboard-default.png",
darkSrc: "https://cdn.uiable.com/block/dashboard-default-dark.png",
alt: "CRM admin dashboard",
href: "/admin-dashboard/orbitcn/preview/dashboard/default",
newTab: true,
},
],
},
]
// ------------------------------ | PREVIEW CARD | ------------------------------ //
function PreviewCard({
preview,
active,
}: {
preview: ShowcasePreview
active?: boolean
}) {
const isExternal = preview.newTab ?? /^https?:\/\//.test(preview.href)
const linkProps = isExternal
? { href: preview.href, target: "_blank" as const, rel: "noreferrer" }
: { href: preview.href }
return (
<Link
{...linkProps}
aria-label={preview.alt}
className={cn(
"group block cursor-none overflow-hidden rounded-xl border bg-muted/40 transition-all",
active ? "border-border" : "border-border"
)}
>
<img
src={preview.src}
alt={preview.alt}
loading="lazy"
className={cn(
"block h-auto w-full object-cover transition-transform duration-500 group-hover:scale-[1.02] lg:h-64",
preview.darkSrc && "dark:hidden"
)}
/>
{preview.darkSrc ? (
<img
src={preview.darkSrc}
alt={preview.alt}
loading="lazy"
className="hidden h-full w-full object-contain transition-transform duration-500 group-hover:scale-[1.02] lg:h-64 dark:block"
/>
) : null}
</Link>
)
}
// ------------------------------ | PREVIEW CAROUSEL | ------------------------------ //
function PreviewCarousel({ previews }: { previews: ShowcasePreview[] }) {
const [active, setActive] = useState(0)
const [paused, setPaused] = useState(false)
useEffect(() => {
if (paused || previews.length <= 1) return
const id = window.setInterval(() => {
setActive((current) => (current + 1) % previews.length)
}, ROTATE_INTERVAL_MS)
return () => window.clearInterval(id)
}, [paused, previews.length])
return (
<div
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
onFocusCapture={() => setPaused(true)}
onBlurCapture={() => setPaused(false)}
>
<CursorArea className="grid grid-cols-2 gap-4 sm:grid-cols-3">
{previews.map((preview, index) => (
<CursorTarget
key={preview.src}
type="view"
className="flex cursor-none items-center justify-center transition-all"
>
<PreviewCard preview={preview} active={index === active} />
</CursorTarget>
))}
</CursorArea>
</div>
)
}
function LandingPageRow({ row }: { row: ShowcaseRow }) {
const isExternal = row.newTab ?? /^https?:\/\//.test(row.href)
const linkProps = isExternal
? { href: row.href, target: "_blank" as const, rel: "noreferrer" }
: { href: row.href }
return (
<div className="grid gap-8 py-10 first:pt-0 last:pb-0 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] lg:items-center lg:gap-12">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<h3 className="text-xl font-medium text-foreground sm:text-2xl">
{row.title}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground sm:text-base">
{row.description}
</p>
</div>
<Button
variant="outline"
nativeButton={false}
render={<Link {...linkProps} />}
className="w-fit gap-2 rounded-lg bg-card hover:bg-accent"
>
Explore more
<IconArrowRight className="size-4" aria-hidden="true" />
</Button>
</div>
<PreviewCarousel previews={row.previews} />
</div>
)
}
// ------------------------------ | LANDING PAGES | ------------------------------ //
export default function TemplatesShowcase() {
return (
<section
className="mx-auto flex w-full scroll-mt-18 flex-col gap-12.5 px-4 py-12.5 sm:px-8"
id="template-showcase"
>
<SectionHeader
title="Shadcn templates for real projects"
titleClassName="tracking-tight"
subtitle="Prebuilt Shadcn templates built with reusable blocks, components, and layouts for real-world projects."
/>
<div className="flex flex-col divide-y divide-border/60">
{rows.map((row) => (
<LandingPageRow key={row.id} row={row} />
))}
</div>
</section>
)
}