Fazekit

Code

"use client";
import * as React from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";
import { Blocks, Gauge, Globe2, LockKeyhole, Sparkles, Workflow, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";

export interface FeatureItem {
  icon: LucideIcon;
  title: string;
  description: string;
}

export interface FeaturesIconGridProps {
  eyebrow?: string;
  title?: React.ReactNode;
  description?: string;
  features?: FeatureItem[];
  className?: string;
}

const DEFAULT_FEATURES: FeatureItem[] = [
  { icon: Gauge, title: "Instant by default", description: "Edge rendering and smart caching keep every page under 100 ms, everywhere your users are." },
  { icon: LockKeyhole, title: "Secure from day one", description: "SSO, audit logs and field-level encryption ship on every plan — no enterprise upsell." },
  { icon: Workflow, title: "Automations that think", description: "Chain triggers, conditions and actions visually, then let the runtime retry on failure." },
  { icon: Globe2, title: "Global by design", description: "Localised content, currencies and time zones handled for you in 40+ regions." },
  { icon: Blocks, title: "Composable blocks", description: "Snap together prebuilt modules or bring your own components with a typed SDK." },
  { icon: Sparkles, title: "AI where it helps", description: "Drafts, summaries and suggestions appear in context — never in the way of real work." },
];

export function FeaturesIconGrid({
  eyebrow = "Why teams switch",
  title = (
    <>
      Everything you need to ship,
      <br className="hidden sm:block" /> <span className="text-muted-foreground">nothing you don&apos;t.</span>
    </>
  ),
  description = "A focused toolkit that replaces a patchwork of plugins, scripts and dashboards with one calm, fast workspace.",
  features = DEFAULT_FEATURES,
  className,
}: FeaturesIconGridProps) {
  const reduce = useReducedMotion();

  const container: Variants = {
    hidden: {},
    show: { transition: { staggerChildren: reduce ? 0 : 0.08, delayChildren: 0.1 } },
  };
  const card: Variants = {
    hidden: reduce ? { opacity: 0 } : { opacity: 0, y: 24, filter: "blur(6px)" },
    show: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] } },
  };

  return (
    <section className={cn("relative w-full overflow-x-clip bg-background py-16 sm:py-20", className)}>
      {/* soft dotted backdrop */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 opacity-60 [background-image:radial-gradient(var(--border)_1px,transparent_1px)] [background-size:22px_22px] [mask-image:radial-gradient(ellipse_70%_60%_at_50%_0%,black,transparent)]"
      />
      <div className="relative mx-auto max-w-6xl px-4 sm:px-6">
        <motion.div
          initial={{ opacity: 0, y: reduce ? 0 : 16 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true, amount: 0.5 }}
          transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
          className="mx-auto max-w-2xl text-center"
        >
          <p className="inline-flex items-center gap-2 rounded-full border bg-card px-3 py-1 text-xs font-medium text-muted-foreground shadow-sm">
            <span className="size-1.5 rounded-full bg-primary" />
            {eyebrow}
          </p>
          <h2 className="mt-5 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl lg:text-[2.75rem] lg:leading-[1.1]">
            {title}
          </h2>
          <p className="mx-auto mt-4 max-w-xl text-pretty text-base text-muted-foreground sm:text-lg">{description}</p>
        </motion.div>

        <motion.ul
          variants={container}
          initial="hidden"
          whileInView="show"
          viewport={{ once: true, amount: 0.15 }}
          className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
        >
          {features.map((f) => (
            <motion.li key={f.title} variants={card} className="h-full">
              <GlowCard feature={f} />
            </motion.li>
          ))}
        </motion.ul>
      </div>
    </section>
  );
}

function GlowCard({ feature }: { feature: FeatureItem }) {
  const ref = React.useRef<HTMLDivElement>(null);
  const Icon = feature.icon;

  const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
    const el = ref.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    el.style.setProperty("--gx", `${e.clientX - r.left}px`);
    el.style.setProperty("--gy", `${e.clientY - r.top}px`);
  };

  return (
    <div
      ref={ref}
      onPointerMove={onMove}
      className="group relative h-full rounded-2xl bg-border p-px transition-shadow duration-300 [--gx:50%] [--gy:0px] hover:shadow-xl hover:shadow-primary/5"
    >
      {/* border glow that follows the cursor */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 transition-opacity duration-300 group-hover:opacity-100"
        style={{ background: "radial-gradient(220px circle at var(--gx) var(--gy), var(--primary), transparent 70%)" }}
      />
      <div className="relative h-full overflow-hidden rounded-[15px] bg-card p-6">
        {/* inner wash */}
        <div
          aria-hidden
          className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
          style={{
            background:
              "radial-gradient(320px circle at var(--gx) var(--gy), color-mix(in oklab, var(--primary) 12%, transparent), transparent 70%)",
          }}
        />
        <div className="relative">
          <div className="relative inline-flex">
            <div
              aria-hidden
              className="absolute -inset-3 rounded-2xl bg-primary/30 opacity-0 blur-xl transition-opacity duration-300 group-hover:opacity-100"
            />
            <div className="relative flex size-11 items-center justify-center rounded-xl border bg-gradient-to-b from-background to-muted text-foreground shadow-sm transition-colors duration-300 group-hover:border-primary/40 group-hover:text-primary">
              <Icon className="size-5" strokeWidth={1.75} aria-hidden />
            </div>
          </div>
          <h3 className="mt-5 text-base font-semibold text-card-foreground">{feature.title}</h3>
          <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{feature.description}</p>
        </div>
      </div>
    </div>
  );
}

More in Features

View all →