Biometric Security
An animated biometric security component with fingerprint scanning, sequential path-by-path fill animation, tilted shine effect, and pulse animations, perfect for showcasing secure login experiences.
Preview
Generate with AI
Want to create this component using AI? Copy the prompt below and paste it into any LLM (ChatGPT, Claude, etc.):
Installation
npx shadcn@latest add https://uiregistry.cappychat.com/registry/biometric-security.jsonInstall dependencies
npm i framer-motion clsx tailwind-merge react-iconsAdd util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Copy the source code
components/ui/biometric-security.tsx
"use client";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useAnimationControls } from "framer-motion";
import { LuCamera, LuSunMedium } from "react-icons/lu";
import { PiLockKeyOpenFill } from "react-icons/pi";
import { MdBattery60 } from "react-icons/md";
import { GiNetworkBars } from "react-icons/gi";
import { cn } from "@/lib/utils";
import { FaPhone } from "react-icons/fa";
interface BiometricSecurityProps {
className?: string;
}
type ScanStatus = "idle" | "scanning" | "unlocked";
// Split fingerprint into multiple paths for sequential animation
const fingerprintPaths = [
"M32.18,105.73c0.7-1.11,2.16-1.44,3.27-0.74c1.11,0.7,1.44,2.16,0.74,3.27l-4.81,7.68c-0.7,1.11-2.16,1.44-3.27,0.74c-1.11-0.7-1.44-2.16-0.74-3.27L32.18,105.73L32.18,105.73z",
"M6.57,75.32c0.33,1.27-0.42,2.57-1.69,2.9c-1.27,0.33-2.57-0.42-2.9-1.69c-0.39-1.5-0.73-3.01-1.02-4.54c-0.28-1.52-0.5-3.03-0.66-4.54c-0.1-0.98-0.18-1.98-0.22-2.99C0.02,63.37,0,62.36,0,61.44C0,44.47,6.88,29.11,18,18C29.11,6.88,44.47,0,61.44,0c16.97,0,32.34,6.83,43.47,17.91c11.11,11.06,17.97,26.36,17.97,43.3c0,1.31-1.07,2.38-2.38,2.38c-1.31,0-2.38-1.07-2.38-2.38c0-15.64-6.33-29.74-16.56-39.93C91.3,11.06,77.12,4.76,61.44,4.76c-15.65,0-29.82,6.34-40.08,16.6C11.11,31.62,4.76,45.79,4.76,61.44c0,1.04,0.02,1.97,0.06,2.8c0.04,0.91,0.11,1.82,0.2,2.73c0.15,1.4,0.35,2.79,0.61,4.17C5.89,72.51,6.2,73.9,6.57,75.32L6.57,75.32z",
"M12.97,96.87c-0.99,0.86-2.49,0.75-3.35-0.24c-0.86-0.99-0.75-2.49,0.24-3.35c0.6-0.52,1.13-1.14,1.58-1.86c0.47-0.74,0.86-1.61,1.17-2.6c1.59-5.06,0.63-10.34-0.34-15.69c-0.66-3.64-1.33-7.31-1.3-11.14c0.12-19.6,12.38-35.58,28.42-43.77c6.73-3.44,14.15-5.5,21.63-5.89c7.51-0.39,15.08,0.93,22.07,4.25C98.7,24,111.38,41.37,114.02,72.13c0.11,1.31-0.87,2.46-2.18,2.57c-1.31,0.11-2.46-0.87-2.57-2.18c-2.47-28.79-14.02-44.89-28.21-51.64c-6.26-2.98-13.05-4.15-19.8-3.81c-6.79,0.35-13.55,2.24-19.71,5.38C26.96,29.89,15.82,44.34,15.71,62c-0.02,3.4,0.61,6.86,1.23,10.29c1.08,5.94,2.14,11.8,0.21,17.95c-0.43,1.38-1,2.62-1.69,3.72C14.76,95.07,13.92,96.04,12.97,96.87L12.97,96.87z",
"M109.22,82.01c0-1.32,1.07-2.38,2.38-2.38s2.38,1.07,2.38,2.38v9.98c0,1.32-1.07,2.38-2.38,2.38s-2.38-1.07-2.38-2.38V82.01L109.22,82.01z",
"M20.01,106.56c-0.98,0.87-2.48,0.78-3.35-0.2c-0.87-0.98-0.78-2.48,0.2-3.35c2.95-2.6,5.1-5.96,6.34-10.17c1.28-4.34,1.6-9.61,0.85-15.92c-1.38-6.78-1.63-13.04-0.82-18.69c0.84-5.91,2.84-11.15,5.89-15.63c0.74-1.08,2.22-1.36,3.3-0.62c1.08,0.74,1.36,2.22,0.62,3.3c-2.64,3.87-4.37,8.44-5.11,13.62c-0.73,5.13-0.5,10.84,0.77,17.07c0.03,0.12,0.06,0.24,0.07,0.36c0.84,6.97,0.45,12.88-1.01,17.85C26.26,99.28,23.63,103.36,20.01,106.56L20.01,106.56z",
"M44.18,34.97c-1.14,0.66-2.59,0.27-3.25-0.87c-0.66-1.14-0.27-2.59,0.87-3.25c2.66-1.54,5.5-2.76,8.43-3.65c8.04-2.44,16.77-2.37,24.71,0.41c7.98,2.8,15.15,8.32,20.03,16.77c1.63,2.81,3,5.96,4.06,9.45c1.51,4.98,2.54,10.54,3.07,16.65c0.52,6.01,0.55,12.61,0.09,19.79c0,0.07-0.01,0.13-0.02,0.2l-1.66,16.38c-0.13,1.3-1.29,2.26-2.6,2.13c-1.3-0.13-2.26-1.29-2.13-2.6l1.66-16.4l0-0.01c0.44-6.91,0.41-13.29-0.09-19.1c-0.5-5.82-1.46-11.05-2.86-15.66c-0.94-3.11-2.17-5.92-3.63-8.45c-4.27-7.4-10.53-12.23-17.48-14.67c-6.99-2.45-14.68-2.51-21.77-0.36C49.04,32.53,46.55,33.6,44.18,34.97L44.18,34.97z",
"M38.95,98.15c-0.23,1.29-1.46,2.16-2.75,1.93c-1.29-0.23-2.16-1.46-1.93-2.75c0.55-3.08,0.86-6.53,0.97-10.29c0.11-3.81,0.02-7.98-0.24-12.44c-0.05-0.96-0.14-2.15-0.22-3.31c-0.54-7.59-0.97-13.71,3.47-21.6c0.98-1.74,2.15-3.36,3.55-4.84c1.4-1.48,3.01-2.82,4.84-3.99c0.12-0.08,0.24-0.14,0.37-0.19c2.71-1.3,5.4-2.31,8.09-2.97c2.77-0.68,5.52-0.98,8.23-0.83c1.31,0.07,2.32,1.18,2.25,2.49c-0.07,1.31-1.18,2.32-2.49,2.25c-2.25-0.12-4.54,0.13-6.87,0.7c-2.32,0.57-4.7,1.46-7.12,2.62c-1.47,0.95-2.75,2.01-3.84,3.17c-1.12,1.19-2.07,2.49-2.86,3.91c-3.74,6.66-3.36,12.14-2.88,18.94c0.07,1,0.14,2.04,0.22,3.38c0.26,4.54,0.35,8.84,0.24,12.83C39.85,91.22,39.53,94.9,38.95,98.15L38.95,98.15z",
"M72.44,44.51c-1.12-0.68-1.47-2.15-0.79-3.26c0.68-1.12,2.14-1.47,3.26-0.79c0.73,0.45,1.44,0.93,2.13,1.45c0.68,0.51,1.35,1.07,2.02,1.68c9.06,8.23,12.13,21.46,12.33,35.3c0.19,13.48-2.34,27.54-4.66,37.91c-0.28,1.28-1.55,2.09-2.83,1.8c-1.28-0.28-2.09-1.55-1.8-2.83c2.26-10.12,4.74-23.81,4.55-36.83c-0.18-12.66-2.88-24.65-10.79-31.84c-0.55-0.5-1.12-0.97-1.69-1.4C73.6,45.25,73.02,44.86,72.44,44.51L72.44,44.51z",
"M43.15,119.69c-0.76,1.07-2.24,1.33-3.31,0.57c-1.07-0.76-1.33-2.24-0.57-3.31c1.68-2.37,3.1-4.97,4.26-7.79c1.16-2.84,2.06-5.93,2.68-9.27c1.16-6.23,0.61-14.17,0.08-21.67c-0.18-2.53-0.35-5-0.46-7.54c-0.16-3.71-0.23-7.35,0.46-10.66c0.75-3.61,2.37-6.74,5.62-9.02c0.73-0.51,1.52-0.97,2.37-1.36c0.85-0.39,1.77-0.74,2.76-1.02c0.74-0.21,1.47-0.38,2.18-0.5c4.87-0.83,8.73,0.43,11.71,3.03c2.83,2.47,4.75,6.11,5.93,10.24c0.35,1.24,0.64,2.53,0.87,3.85c0.12,0.72,0.23,1.45,0.31,2.18c0.08,0.72,0.15,1.46,0.2,2.22c0.5,7.54,0.17,16.95-0.85,26.16c-0.98,8.83-2.6,17.53-4.75,24.3c-0.4,1.25-1.73,1.94-2.98,1.55c-1.25-0.4-1.94-1.73-1.55-2.98c2.04-6.43,3.59-14.81,4.54-23.38c0.99-8.95,1.31-18.06,0.83-25.34c-0.04-0.63-0.1-1.29-0.18-1.97c-0.07-0.64-0.17-1.28-0.28-1.91c-0.2-1.14-0.45-2.26-0.75-3.35c-0.94-3.31-2.4-6.15-4.48-7.96c-1.92-1.67-4.47-2.47-7.77-1.91c-0.53,0.09-1.09,0.22-1.68,0.39c-0.77,0.22-1.46,0.47-2.07,0.76c-0.62,0.29-1.17,0.6-1.64,0.93c-2.09,1.46-3.16,3.59-3.68,6.09c-0.58,2.79-0.51,6.1-0.37,9.49c0.1,2.25,0.28,4.81,0.46,7.43c0.54,7.78,1.12,16.01-0.16,22.85c-0.68,3.65-1.67,7.04-2.96,10.2C46.64,114.11,45.04,117.02,43.15,119.69L43.15,119.69z",
"M59.44,62.15c-0.24-1.29,0.62-2.53,1.91-2.76c1.29-0.24,2.53,0.62,2.76,1.91c1.21,6.52,1.79,13.11,1.81,19.73c0.02,6.59-0.52,13.21-1.54,19.84c-0.2,1.29-1.41,2.18-2.71,1.98c-1.29-0.2-2.18-1.41-1.98-2.71c0.99-6.38,1.51-12.76,1.49-19.11C61.16,74.7,60.6,68.4,59.44,62.15L59.44,62.15z",
"M56.72,108.31c0.28-1.28,1.55-2.09,2.83-1.8c1.28,0.28,2.09,1.55,1.8,2.83l-2.08,9.37c-0.28,1.28-1.55,2.09-2.83,1.8c-1.28-0.28-2.09-1.55-1.8-2.83L56.72,108.31L56.72,108.31z",
];
const shineVariants = {
initial: { x: "-200%", opacity: 0 },
active: {
x: "200%",
opacity: [0, 0.8, 0],
transition: {
duration: 1.2,
ease: "easeInOut" as const,
},
},
};
const pulseVariants = {
initial: { opacity: 0, scale: 0.9 },
active: {
opacity: [0.45, 0],
scale: [1, 1.4],
transition: {
duration: 0.9,
ease: "easeOut" as const,
},
},
};
export const BiometricSecurity = memo(
({ className }: BiometricSecurityProps) => {
const [status, setStatus] = useState<ScanStatus>("idle");
const shineControls = useAnimationControls();
const pulseControls = useAnimationControls();
const resetRef = useRef<NodeJS.Timeout | null>(null);
const control0 = useAnimationControls();
const control1 = useAnimationControls();
const control2 = useAnimationControls();
const control3 = useAnimationControls();
const control4 = useAnimationControls();
const control5 = useAnimationControls();
const control6 = useAnimationControls();
const control7 = useAnimationControls();
const control8 = useAnimationControls();
const control9 = useAnimationControls();
const control10 = useAnimationControls();
const pathControls = useRef<ReturnType<typeof useAnimationControls>[]>([
control0,
control1,
control2,
control3,
control4,
control5,
control6,
control7,
control8,
control9,
control10,
]);
const clearReset = useCallback(() => {
if (resetRef.current) {
clearTimeout(resetRef.current);
resetRef.current = null;
}
}, []);
const applyInitialState = useCallback(() => {
// Reset all paths to filled neutral state
pathControls.current.forEach((control) => {
control.set({
pathLength: 1,
fill: "#525252",
stroke: "#525252",
opacity: 0.8,
});
});
shineControls.set(shineVariants.initial);
pulseControls.set(pulseVariants.initial);
}, [pulseControls, shineControls]);
useEffect(() => {
applyInitialState();
return () => {
clearReset();
};
}, [applyInitialState, clearReset]);
const scheduleReset = useCallback(() => {
clearReset();
resetRef.current = setTimeout(() => {
applyInitialState();
setStatus("idle");
resetRef.current = null;
}, 1000);
}, [applyInitialState, clearReset]);
const startScan = useCallback(async () => {
if (status === "scanning") {
return;
}
clearReset();
applyInitialState();
setStatus("scanning");
try {
// Animate each path sequentially with fill
for (let i = 0; i < pathControls.current.length; i++) {
pathControls.current[i].start({
fill: "#22d3ee",
stroke: "#22d3ee",
opacity: 1,
transition: {
duration: 0.15,
ease: "easeOut" as const,
},
});
await new Promise((resolve) => setTimeout(resolve, 80));
}
setStatus("unlocked");
// Add shine effect
await shineControls.start("active");
await pulseControls.start("active");
scheduleReset();
} catch {
setStatus("idle");
}
}, [
applyInitialState,
clearReset,
pulseControls,
shineControls,
scheduleReset,
status,
]);
return (
<div
className={cn(
"flex w-full items-center justify-center py-8 text-neutral-100",
className
)}
>
<div className="relative flex w-full max-w-[300px] md:max-w-[320px] flex-col items-center">
<div className="absolute inset-6 rounded-[38px] border border-white/10" />
<div className="relative w-full rounded-[38px] min-h-[500px] border-8 border-neutral-500/60 dark:border-neutral-800/80 bg-neutral-400/70 dark:bg-neutral-950/95 px-6 pb-9 pt-12 ">
<div className="pointer-events-none border-4 border-neutral-700 absolute inset-x-[46%] top-5 h-3 w-3 rounded-full bg-neutral-800" />
<div className="flex gap-1 justify-center absolute top-4 right-5">
<GiNetworkBars className="h-4 w-4 my-auto text-white" />
<MdBattery60 className="h-6 w-6 rotate-90 text-white" />
<span className="text-xs my-auto text-white">70%</span>
</div>
<div className="space-y-1 mt-8 text-center font-medium">
<div className="text-sm text-white">Saturday</div>
<div className="text-4xl font-semibold text-white">7:24 PM</div>
<div className="flex items-center justify-center gap-2 text-sm text-white">
<span>9 Sept</span>
<span className="flex items-center gap-1 rounded-full bg-neutral-500/70 dark:bg-neutral-900/80 px-2 py-1 text-cyan-200">
<LuSunMedium className="h-3.5 w-3.5" />
26{"\u00B0"}
</span>
</div>
</div>
<div className="mt-16 absolute bottom-5 left-1/2 -translate-x-1/2 flex gap-7 items-end justify-between text-neutral-500">
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-neutral-700/70 bg-neutral-500 dark:bg-neutral-900/80 text-cyan-200">
<FaPhone className="h-4 w-4 text-white" />
</div>
<motion.button
type="button"
aria-label="Unlock with biometric scan"
className="relative flex h-24 w-24 mb-3 items-center justify-center rounded-full bg-neutral-900/70 p-4 text-cyan-200/80 shadow-sm shadow-neutral-500/10 outline-none transition focus-visible:ring-2 focus-visible:ring-cyan-400/70"
whileHover={{ scale: 1 }}
whileTap={{ scale: 0.98 }}
onHoverStart={startScan}
onTapStart={startScan}
onFocus={startScan}
>
<div className="absolute inset-0 rounded-full border border-enutral-500/20 bg-gradient-to-br dark:from-neutral-900 dark:to-neutral-950 from-neutral-400 to-neutral-500" />
<motion.div
className="absolute inset-0 rounded-full border border-cyan-400/70"
animate={pulseControls}
variants={pulseVariants}
/>
<div className="relative z-10 flex h-full w-full items-center justify-center">
<div className="absolute inset-1 rounded-[24px] bg-neutral-500/60 dark:bg-neutral-500/10 blur-md" />
<svg
viewBox="0 0 122.88 121.74"
className="relative z-10 h-16 w-16"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
>
{fingerprintPaths.map((path, index) => (
<motion.path
key={index}
d={path}
animate={pathControls.current[index]}
initial={{
pathLength: 1,
fill: "#525252",
stroke: "#525252",
opacity: 0.8,
}}
/>
))}
</svg>
{/* Tilted shine effect on success */}
<div className="absolute inset-0 z-20 overflow-hidden rounded-[28px]">
<motion.div
className="absolute inset-y-0 -left-8 w-12 bg-gradient-to-r from-transparent via-cyan-200/70 to-transparent"
style={{ transform: "skewX(-20deg)" }}
animate={shineControls}
variants={shineVariants}
/>
</div>
</div>
<AnimatePresence>
{status === "unlocked" && (
<motion.div
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.38, ease: "easeOut" }}
className="absolute -top-12 flex items-center gap-2 rounded-full border border-cyan-400/60 bg-neutral-800/70 dark:bg-neutral-900/90 px-3 py-1 text-xs font-medium text-cyan-100 shadow-[0_10px_30px_rgba(14,116,144,0.35)]"
>
<PiLockKeyOpenFill className="h-4 w-4" />
Unlocked
</motion.div>
)}
</AnimatePresence>
</motion.button>
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-neutral-700/70 bg-neutral-500 dark:bg-neutral-900/80">
<LuCamera className="h-5 w-5 text-neutral-100" />
</div>
</div>
</div>
<div className="absolute inset-2 -z-10 rounded-[46px] border border-cyan-500/20" />
</div>
</div>
);
}
);
BiometricSecurity.displayName = "BiometricSecurity";
Usage
import { BiometricSecurity } from "@/components/ui/biometric-security";
export default function MyComponent() {
return (
<div className="min-h-screen bg-neutral-950 flex items-center justify-center">
<BiometricSecurity />
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Optional additional CSS classes |
Domed Grid
A wide isometric grid backdrop — 9x9 cuboids forming a soft domed plateau with a terracotta-checkered center. Designed to sit behind hero/CTA copy as a dimmed decorative layer.
Book Appointment
An interactive appointment booking component with calendar and time slot selection. Features month navigation, booked date indicators, and real-time selection display.
