# Lock screen notifications

> A live clock over a stack of notifications that fans out on tap and lets you swipe each one away.

- Category: [Liquid Glass](https://library.aimran.es/components/liquid-glass)
- Built with: React + Motion
- Live editor: https://library.aimran.es/blocks/lg-lock-notifications
- Library: AIMRAN Library (https://library.aimran.es), free, no sign-up, by Aimran Oukham

## Where to use it

If your product sends notifications, this lock screen is a great way to show them off on a landing page: the reminders of a calendar app, the messages of a chat app or the order updates of a delivery service. The iOS notification stack groups them, and one tap spreads them out.

Replace the sample messages with real ones from your product, written the way your app would really send them. That does more for the pitch than any headline. Swiping to delete also works with a mouse, which makes it nice for demos.

## Code

### React (Motion)

```tsx
import { useEffect, useRef, useState, type CSSProperties, type MouseEvent, type ReactNode } from "react";
import { AnimatePresence, motion, useMotionValue, useTransform } from "framer-motion";
import { Bell, Calendar, Image as ImageIcon, Mail, MessageCircle, Music2, type LucideIcon } from "lucide-react";

type Wallpaper = "aurora" | "sunset" | "ocean" | "light";

const spring = { type: "spring" as const, stiffness: 400, damping: 30 };

const walls: Record<Wallpaper, { base: string; blobs: string[] }> = {
  aurora: { base: "#0b0b1a", blobs: ["#5b21b6", "#2563eb", "#ec4899"] },
  sunset: { base: "#1a0b14", blobs: ["#f97316", "#f43f5e", "#8b5cf6"] },
  ocean: { base: "#031a1f", blobs: ["#06b6d4", "#3b82f6", "#10b981"] },
  light: { base: "#f8fafc", blobs: ["#fed7aa", "#ddd6fe", "#bae6fd"] },
};

const blobPositions = [
  ["-10%", "-15%"],
  ["45%", "5%"],
  ["5%", "50%"],
];

const grain = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")";

const glassCss = `
.lg-font {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", sans-serif;
  -webkit-font-smoothing: antialiased;
}
.lg-glass {
  position: relative;
  overflow: hidden;
  background: rgba(255, 255, 255, var(--lg-a, 0.12));
  -webkit-backdrop-filter: blur(var(--lg-blur, 20px)) saturate(180%);
  backdrop-filter: blur(var(--lg-blur, 20px)) saturate(180%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.5),
    inset 0 -1px 0 rgba(255, 255, 255, 0.1),
    inset 0 0 20px rgba(255, 255, 255, 0.08),
    0 8px 32px rgba(0, 0, 0, 0.18);
}
/* Borde de luz */
.lg-glass::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  padding: 1px;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.05) 40%, rgba(255, 255, 255, 0.05) 60%, rgba(255, 255, 255, 0.4));
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}
/* Reflejo que sigue al cursor (--x / --y los pone track()) */
.lg-glass::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--lg-hover, 0);
  transition: opacity 0.3s;
  background: radial-gradient(circle 140px at var(--x, 50%) var(--y, 0%), rgba(255, 255, 255, var(--lg-shine, 0.25)), transparent 70%);
}
.lg-glass:hover {
  --lg-hover: 1;
}
.lg-text {
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}
.lg-light .lg-text {
  color: #1d1d1f;
  text-shadow: none;
}
@keyframes lg-drift {
  0%, 100% { transform: translate(0, 0) scale(1); }
  33% { transform: translate(8%, -6%) scale(1.12); }
  66% { transform: translate(-6%, 7%) scale(0.94); }
}
`;

// Guarda la posición del cursor para el reflejo de .lg-glass::after
function track(e: MouseEvent<HTMLElement>) {
  const r = e.currentTarget.getBoundingClientRect();
  e.currentTarget.style.setProperty("--x", `${e.clientX - r.left}px`);
  e.currentTarget.style.setProperty("--y", `${e.clientY - r.top}px`);
}

interface StageProps {
  bg: Wallpaper;
  blur: number;
  opacity: number;
  shine: number;
  className?: string;
  children: ReactNode;
}

// Fondo de degradado animado sobre el que se ve el cristal
function Stage({
  bg,
  blur,
  opacity,
  shine,
  className = "relative h-[560px] w-full",
  children,
}: StageProps) {
  const light = bg === "light";
  const wall = walls[bg] ?? walls.aurora;
  const vars = {
    "--lg-blur": `${blur}px`,
    "--lg-a": light ? Math.min(0.6, opacity / 100 + 0.23) : opacity / 100,
    "--lg-shine": shine / 100,
  } as CSSProperties;

  return (
    <div
      className={`lg-font overflow-hidden rounded-[22px] ${className} ${light ? "lg-light" : ""}`}
      style={vars}
    >
      <style>{glassCss}</style>
      <div className="absolute inset-0 overflow-hidden" style={{ background: wall.base }}>
        <div className="absolute inset-0" style={{ filter: "blur(60px)" }}>
          {wall.blobs.map((color, i) => (
            <div
              key={i}
              className="absolute h-[75%] w-[75%] rounded-full"
              style={{
                left: blobPositions[i]?.[0],
                top: blobPositions[i]?.[1],
                background: `radial-gradient(circle, ${color} 0%, transparent 68%)`,
                opacity: light ? 1 : 0.9,
                animation: `lg-drift 20s ease-in-out ${-i * 6}s infinite`,
              }}
            />
          ))}
        </div>
        <div
          className="absolute inset-0"
          style={{ backgroundImage: grain, opacity: 0.04, mixBlendMode: "overlay" }}
        />
      </div>
      <div className="relative grid h-full w-full grid-cols-[100%] place-items-center p-4">{children}</div>
    </div>
  );
}

type Notif = {
  id: number;
  app: string;
  color: string;
  icon: LucideIcon;
  title: string;
  text: string;
  time: string;
};

const pool: Omit<Notif, "id">[] = [
  { app: "Messages", color: "linear-gradient(#5ef079,#27c24c)", icon: MessageCircle, title: "Lucía", text: "Meet at 8 on the terrace?", time: "now" },
  { app: "Mail", color: "linear-gradient(#5ab8ff,#1a7cf5)", icon: Mail, title: "Weekly summary", text: "All 3 of your projects moved forward this week.", time: "5 min" },
  { app: "Calendar", color: "linear-gradient(#ff7a6b,#f2493a)", icon: Calendar, title: "Design · 10:30 AM", text: "Prototype review with the team.", time: "20 min" },
  { app: "Music", color: "linear-gradient(#ff6b93,#f3344f)", icon: Music2, title: "New release", text: "The album you saved is now available.", time: "1 h" },
  { app: "Photos", color: "linear-gradient(#ffd66b,#ff9f0a)", icon: ImageIcon, title: "Memories", text: "On this day, one year ago.", time: "now" },
];

// La hora se fija tras montar para no desajustar el render del servidor
function useClock() {
  const [date, setDate] = useState<Date | null>(null);
  useEffect(() => {
    setDate(new Date());
    const t = setInterval(() => setDate(new Date()), 1000);
    return () => clearInterval(t);
  }, []);
  return date;
}

// Desliza a la izquierda para mostrar "Clear"; si arrastras lejos, se borra sola
function NotifRow({ n, onDelete }: { n: Notif; onDelete: () => void }) {
  const x = useMotionValue(0);
  const deleteOpacity = useTransform(x, [-90, -30], [1, 0]);
  const Icon = n.icon;

  return (
    <div className="relative">
      <motion.button
        type="button"
        style={{ opacity: deleteOpacity }}
        onClick={onDelete}
        className="absolute inset-y-0 right-0 grid w-[84px] place-items-center rounded-[22px] bg-[#ff3b30] text-[14px] font-semibold text-white"
      >
        Clear
      </motion.button>
      <motion.div
        drag="x"
        dragConstraints={{ left: -96, right: 0 }}
        dragElastic={0.1}
        style={{ x }}
        onDragEnd={(_, info) => {
          if (info.offset.x < -150) onDelete();
          else x.set(info.offset.x < -50 ? -96 : 0);
        }}
        onMouseMove={track}
        className="lg-glass lg-text flex items-center gap-3 rounded-[22px] p-3"
      >
        <div
          className="relative z-[3] grid h-[38px] w-[38px] shrink-0 place-items-center rounded-[10px]"
          style={{ background: n.color }}
        >
          <Icon className="h-5 w-5 text-white" strokeWidth={1.75} />
        </div>
        <div className="relative z-[3] min-w-0 flex-1">
          <div className="flex justify-between gap-2">
            <p className="truncate text-[14px] font-semibold">{n.title}</p>
            <span className="shrink-0 text-[12px] opacity-60">{n.time}</span>
          </div>
          <p className="truncate text-[13px] opacity-80">{n.text}</p>
        </div>
      </motion.div>
    </div>
  );
}

interface LockNotificationsProps {
  label?: string;
  bg?: Wallpaper;
  blur?: number;
  opacity?: number;
  shine?: number;
}

export default function LockNotifications({
  label = "Get a notification",
  bg = "aurora",
  blur = 20,
  opacity = 12,
  shine = 25,
}: LockNotificationsProps) {
  const glass = { bg, blur, opacity, shine };
  const now = useClock();
  const nextId = useRef(10);
  const [list, setList] = useState<Notif[]>(() => pool.slice(0, 4).map((p, i) => ({ ...p, id: i })));
  const [open, setOpen] = useState(false);

  const time = now ? now.toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" }) : "9:41";
  const date = now ? now.toLocaleDateString("es-ES", { weekday: "long", day: "numeric", month: "long" }) : "";

  const add = () => {
    const p = pool[nextId.current % pool.length];
    if (!p) return;
    setList((l) => [{ ...p, time: "now", id: nextId.current++ }, ...l]);
  };

  return (
    <Stage {...glass}>
      <div className="absolute inset-0 flex flex-col items-center overflow-hidden px-4 pt-8">
        <p className="lg-text text-[17px] font-medium capitalize">{date}</p>
        <p className="lg-text text-[90px] font-extralight leading-none tracking-[-0.04em] tabular-nums">{time}</p>
        <div className="mt-6 w-full max-w-[360px]">
          {open ? (
            <motion.div layout className="flex flex-col gap-2">
              <AnimatePresence initial={false}>
                {list.map((n) => (
                  <motion.div
                    key={n.id}
                    layout
                    initial={{ opacity: 0, y: -40, scale: 0.9 }}
                    animate={{ opacity: 1, y: 0, scale: 1 }}
                    exit={{ opacity: 0, x: -300, scale: 0.9 }}
                    transition={spring}
                  >
                    <NotifRow n={n} onDelete={() => setList((l) => l.filter((q) => q.id !== n.id))} />
                  </motion.div>
                ))}
              </AnimatePresence>
              {list.length > 0 && (
                <motion.button
                  type="button"
                  whileTap={{ scale: 0.95 }}
                  transition={spring}
                  onClick={() => setOpen(false)}
                  className="lg-glass lg-text mx-auto mt-1 h-8 rounded-full px-4 text-[13px] font-medium"
                >
                  <span className="relative z-[3]">Show less</span>
                </motion.button>
              )}
            </motion.div>
          ) : (
            // Pila cerrada: hasta 3 tarjetas, solo la primera con contenido
            <button
              type="button"
              onClick={() => setOpen(true)}
              className="relative block w-full"
              style={{ height: list.length ? 76 + Math.min(list.length - 1, 2) * 10 : 0 }}
            >
              <AnimatePresence initial={false}>
                {list.slice(0, 3).map((n, i) => (
                  <motion.div
                    key={n.id}
                    className="absolute inset-x-0 top-0"
                    style={{ zIndex: 10 - i }}
                    initial={{ y: -80, opacity: 0, scale: 1 }}
                    animate={{ y: i * 10, scale: 1 - i * 0.05, opacity: i === 0 ? 1 : 0.75 }}
                    exit={{ opacity: 0, scale: 0.9 }}
                    transition={spring}
                  >
                    <div
                      className="lg-glass lg-text flex items-center gap-3 rounded-[22px] p-3 text-left"
                      style={i ? { minHeight: 64 } : {}}
                    >
                      {i === 0 && (
                        <>
                          <div
                            className="relative z-[3] grid h-[38px] w-[38px] shrink-0 place-items-center rounded-[10px]"
                            style={{ background: n.color }}
                          >
                            <n.icon className="h-5 w-5 text-white" strokeWidth={1.75} />
                          </div>
                          <div className="relative z-[3] min-w-0 flex-1">
                            <div className="flex justify-between gap-2">
                              <p className="truncate text-[14px] font-semibold">{n.title}</p>
                              <span className="text-[12px] opacity-60">{n.time}</span>
                            </div>
                            <p className="truncate text-[13px] opacity-80">
                              {n.text}
                              {list.length > 1 ? ` · +${list.length - 1} más` : ""}
                            </p>
                          </div>
                        </>
                      )}
                    </div>
                  </motion.div>
                ))}
              </AnimatePresence>
            </button>
          )}
        </div>
        <motion.button
          type="button"
          onMouseMove={track}
          whileTap={{ scale: 0.95 }}
          transition={spring}
          onClick={add}
          className="lg-glass lg-text mb-6 mt-auto flex h-11 shrink-0 items-center gap-2 rounded-full px-5 text-[15px] font-medium"
        >
          <Bell className="relative z-[3] h-4 w-4" strokeWidth={1.75} />
          <span className="relative z-[3]">{label}</span>
        </motion.button>
      </div>
    </Stage>
  );
}
```

## Prompt for AI assistants

Paste this into ChatGPT, Claude, Cursor, v0 or Lovable to adapt the block to your stack (Tailwind, Vue, Svelte…):

````text
I want to add the “Lock screen notifications” block (Liquid Glass) from AIMRAN Library to my project.

## What it is
A live clock over a stack of notifications that fans out on tap and lets you swipe each one away.

## Chosen settings
- Text: "Get a notification"
- Background: Aurora (other options: Sunset, Ocean, Light)
- Blur: 20px (from 4 to 40px)
- Glass opacity: 12% (from 4 to 40%)
- Shine: 25% (from 0 to 60%)

## What I need
1. Add it to my project with the same look and animation as the reference code: same sizes, colours, radii, shadows, timings and easing.
2. It is built with React + TypeScript, with framer-motion for the animation and lucide-react for the icons. If my project uses something else (Vue, Svelte, Astro, plain HTML…), translate it without losing any behaviour. If I use Tailwind, turn the styles into classes; if not, keep the CSS in its own file.
3. Expose the settings above as props (or CSS variables) with those values as defaults.
4. Make it work well on phones: nothing that depends only on hover, touch targets of at least 44 px and no overflow on 320 px screens.
5. Make it look right in light and dark mode.
6. Keep it accessible: semantic HTML, roles and aria-* where needed, visible focus and keyboard support.
7. Respect prefers-reduced-motion: with reduced motion, no big animations but the same end result.

## Reference code
```tsx
import { useEffect, useRef, useState, type CSSProperties, type MouseEvent, type ReactNode } from "react";
import { AnimatePresence, motion, useMotionValue, useTransform } from "framer-motion";
import { Bell, Calendar, Image as ImageIcon, Mail, MessageCircle, Music2, type LucideIcon } from "lucide-react";

type Wallpaper = "aurora" | "sunset" | "ocean" | "light";

const spring = { type: "spring" as const, stiffness: 400, damping: 30 };

const walls: Record<Wallpaper, { base: string; blobs: string[] }> = {
  aurora: { base: "#0b0b1a", blobs: ["#5b21b6", "#2563eb", "#ec4899"] },
  sunset: { base: "#1a0b14", blobs: ["#f97316", "#f43f5e", "#8b5cf6"] },
  ocean: { base: "#031a1f", blobs: ["#06b6d4", "#3b82f6", "#10b981"] },
  light: { base: "#f8fafc", blobs: ["#fed7aa", "#ddd6fe", "#bae6fd"] },
};

const blobPositions = [
  ["-10%", "-15%"],
  ["45%", "5%"],
  ["5%", "50%"],
];

const grain = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")";

const glassCss = `
.lg-font {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", sans-serif;
  -webkit-font-smoothing: antialiased;
}
.lg-glass {
  position: relative;
  overflow: hidden;
  background: rgba(255, 255, 255, var(--lg-a, 0.12));
  -webkit-backdrop-filter: blur(var(--lg-blur, 20px)) saturate(180%);
  backdrop-filter: blur(var(--lg-blur, 20px)) saturate(180%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.5),
    inset 0 -1px 0 rgba(255, 255, 255, 0.1),
    inset 0 0 20px rgba(255, 255, 255, 0.08),
    0 8px 32px rgba(0, 0, 0, 0.18);
}
/* Borde de luz */
.lg-glass::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  padding: 1px;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.05) 40%, rgba(255, 255, 255, 0.05) 60%, rgba(255, 255, 255, 0.4));
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}
/* Reflejo que sigue al cursor (--x / --y los pone track()) */
.lg-glass::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--lg-hover, 0);
  transition: opacity 0.3s;
  background: radial-gradient(circle 140px at var(--x, 50%) var(--y, 0%), rgba(255, 255, 255, var(--lg-shine, 0.25)), transparent 70%);
}
.lg-glass:hover {
  --lg-hover: 1;
}
.lg-text {
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}
.lg-light .lg-text {
  color: #1d1d1f;
  text-shadow: none;
}
@keyframes lg-drift {
  0%, 100% { transform: translate(0, 0) scale(1); }
  33% { transform: translate(8%, -6%) scale(1.12); }
  66% { transform: translate(-6%, 7%) scale(0.94); }
}
`;

// Guarda la posición del cursor para el reflejo de .lg-glass::after
function track(e: MouseEvent<HTMLElement>) {
  const r = e.currentTarget.getBoundingClientRect();
  e.currentTarget.style.setProperty("--x", `${e.clientX - r.left}px`);
  e.currentTarget.style.setProperty("--y", `${e.clientY - r.top}px`);
}

interface StageProps {
  bg: Wallpaper;
  blur: number;
  opacity: number;
  shine: number;
  className?: string;
  children: ReactNode;
}

// Fondo de degradado animado sobre el que se ve el cristal
function Stage({
  bg,
  blur,
  opacity,
  shine,
  className = "relative h-[560px] w-full",
  children,
}: StageProps) {
  const light = bg === "light";
  const wall = walls[bg] ?? walls.aurora;
  const vars = {
    "--lg-blur": `${blur}px`,
    "--lg-a": light ? Math.min(0.6, opacity / 100 + 0.23) : opacity / 100,
    "--lg-shine": shine / 100,
  } as CSSProperties;

  return (
    <div
      className={`lg-font overflow-hidden rounded-[22px] ${className} ${light ? "lg-light" : ""}`}
      style={vars}
    >
      <style>{glassCss}</style>
      <div className="absolute inset-0 overflow-hidden" style={{ background: wall.base }}>
        <div className="absolute inset-0" style={{ filter: "blur(60px)" }}>
          {wall.blobs.map((color, i) => (
            <div
              key={i}
              className="absolute h-[75%] w-[75%] rounded-full"
              style={{
                left: blobPositions[i]?.[0],
                top: blobPositions[i]?.[1],
                background: `radial-gradient(circle, ${color} 0%, transparent 68%)`,
                opacity: light ? 1 : 0.9,
                animation: `lg-drift 20s ease-in-out ${-i * 6}s infinite`,
              }}
            />
          ))}
        </div>
        <div
          className="absolute inset-0"
          style={{ backgroundImage: grain, opacity: 0.04, mixBlendMode: "overlay" }}
        />
      </div>
      <div className="relative grid h-full w-full grid-cols-[100%] place-items-center p-4">{children}</div>
    </div>
  );
}

type Notif = {
  id: number;
  app: string;
  color: string;
  icon: LucideIcon;
  title: string;
  text: string;
  time: string;
};

const pool: Omit<Notif, "id">[] = [
  { app: "Messages", color: "linear-gradient(#5ef079,#27c24c)", icon: MessageCircle, title: "Lucía", text: "Meet at 8 on the terrace?", time: "now" },
  { app: "Mail", color: "linear-gradient(#5ab8ff,#1a7cf5)", icon: Mail, title: "Weekly summary", text: "All 3 of your projects moved forward this week.", time: "5 min" },
  { app: "Calendar", color: "linear-gradient(#ff7a6b,#f2493a)", icon: Calendar, title: "Design · 10:30 AM", text: "Prototype review with the team.", time: "20 min" },
  { app: "Music", color: "linear-gradient(#ff6b93,#f3344f)", icon: Music2, title: "New release", text: "The album you saved is now available.", time: "1 h" },
  { app: "Photos", color: "linear-gradient(#ffd66b,#ff9f0a)", icon: ImageIcon, title: "Memories", text: "On this day, one year ago.", time: "now" },
];

// La hora se fija tras montar para no desajustar el render del servidor
function useClock() {
  const [date, setDate] = useState<Date | null>(null);
  useEffect(() => {
    setDate(new Date());
    const t = setInterval(() => setDate(new Date()), 1000);
    return () => clearInterval(t);
  }, []);
  return date;
}

// Desliza a la izquierda para mostrar "Clear"; si arrastras lejos, se borra sola
function NotifRow({ n, onDelete }: { n: Notif; onDelete: () => void }) {
  const x = useMotionValue(0);
  const deleteOpacity = useTransform(x, [-90, -30], [1, 0]);
  const Icon = n.icon;

  return (
    <div className="relative">
      <motion.button
        type="button"
        style={{ opacity: deleteOpacity }}
        onClick={onDelete}
        className="absolute inset-y-0 right-0 grid w-[84px] place-items-center rounded-[22px] bg-[#ff3b30] text-[14px] font-semibold text-white"
      >
        Clear
      </motion.button>
      <motion.div
        drag="x"
        dragConstraints={{ left: -96, right: 0 }}
        dragElastic={0.1}
        style={{ x }}
        onDragEnd={(_, info) => {
          if (info.offset.x < -150) onDelete();
          else x.set(info.offset.x < -50 ? -96 : 0);
        }}
        onMouseMove={track}
        className="lg-glass lg-text flex items-center gap-3 rounded-[22px] p-3"
      >
        <div
          className="relative z-[3] grid h-[38px] w-[38px] shrink-0 place-items-center rounded-[10px]"
          style={{ background: n.color }}
        >
          <Icon className="h-5 w-5 text-white" strokeWidth={1.75} />
        </div>
        <div className="relative z-[3] min-w-0 flex-1">
          <div className="flex justify-between gap-2">
            <p className="truncate text-[14px] font-semibold">{n.title}</p>
            <span className="shrink-0 text-[12px] opacity-60">{n.time}</span>
          </div>
          <p className="truncate text-[13px] opacity-80">{n.text}</p>
        </div>
      </motion.div>
    </div>
  );
}

interface LockNotificationsProps {
  label?: string;
  bg?: Wallpaper;
  blur?: number;
  opacity?: number;
  shine?: number;
}

export default function LockNotifications({
  label = "Get a notification",
  bg = "aurora",
  blur = 20,
  opacity = 12,
  shine = 25,
}: LockNotificationsProps) {
  const glass = { bg, blur, opacity, shine };
  const now = useClock();
  const nextId = useRef(10);
  const [list, setList] = useState<Notif[]>(() => pool.slice(0, 4).map((p, i) => ({ ...p, id: i })));
  const [open, setOpen] = useState(false);

  const time = now ? now.toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" }) : "9:41";
  const date = now ? now.toLocaleDateString("es-ES", { weekday: "long", day: "numeric", month: "long" }) : "";

  const add = () => {
    const p = pool[nextId.current % pool.length];
    if (!p) return;
    setList((l) => [{ ...p, time: "now", id: nextId.current++ }, ...l]);
  };

  return (
    <Stage {...glass}>
      <div className="absolute inset-0 flex flex-col items-center overflow-hidden px-4 pt-8">
        <p className="lg-text text-[17px] font-medium capitalize">{date}</p>
        <p className="lg-text text-[90px] font-extralight leading-none tracking-[-0.04em] tabular-nums">{time}</p>
        <div className="mt-6 w-full max-w-[360px]">
          {open ? (
            <motion.div layout className="flex flex-col gap-2">
              <AnimatePresence initial={false}>
                {list.map((n) => (
                  <motion.div
                    key={n.id}
                    layout
                    initial={{ opacity: 0, y: -40, scale: 0.9 }}
                    animate={{ opacity: 1, y: 0, scale: 1 }}
                    exit={{ opacity: 0, x: -300, scale: 0.9 }}
                    transition={spring}
                  >
                    <NotifRow n={n} onDelete={() => setList((l) => l.filter((q) => q.id !== n.id))} />
                  </motion.div>
                ))}
              </AnimatePresence>
              {list.length > 0 && (
                <motion.button
                  type="button"
                  whileTap={{ scale: 0.95 }}
                  transition={spring}
                  onClick={() => setOpen(false)}
                  className="lg-glass lg-text mx-auto mt-1 h-8 rounded-full px-4 text-[13px] font-medium"
                >
                  <span className="relative z-[3]">Show less</span>
                </motion.button>
              )}
            </motion.div>
          ) : (
            // Pila cerrada: hasta 3 tarjetas, solo la primera con contenido
            <button
              type="button"
              onClick={() => setOpen(true)}
              className="relative block w-full"
              style={{ height: list.length ? 76 + Math.min(list.length - 1, 2) * 10 : 0 }}
            >
              <AnimatePresence initial={false}>
                {list.slice(0, 3).map((n, i) => (
                  <motion.div
                    key={n.id}
                    className="absolute inset-x-0 top-0"
                    style={{ zIndex: 10 - i }}
                    initial={{ y: -80, opacity: 0, scale: 1 }}
                    animate={{ y: i * 10, scale: 1 - i * 0.05, opacity: i === 0 ? 1 : 0.75 }}
                    exit={{ opacity: 0, scale: 0.9 }}
                    transition={spring}
                  >
                    <div
                      className="lg-glass lg-text flex items-center gap-3 rounded-[22px] p-3 text-left"
                      style={i ? { minHeight: 64 } : {}}
                    >
                      {i === 0 && (
                        <>
                          <div
                            className="relative z-[3] grid h-[38px] w-[38px] shrink-0 place-items-center rounded-[10px]"
                            style={{ background: n.color }}
                          >
                            <n.icon className="h-5 w-5 text-white" strokeWidth={1.75} />
                          </div>
                          <div className="relative z-[3] min-w-0 flex-1">
                            <div className="flex justify-between gap-2">
                              <p className="truncate text-[14px] font-semibold">{n.title}</p>
                              <span className="text-[12px] opacity-60">{n.time}</span>
                            </div>
                            <p className="truncate text-[13px] opacity-80">
                              {n.text}
                              {list.length > 1 ? ` · +${list.length - 1} más` : ""}
                            </p>
                          </div>
                        </>
                      )}
                    </div>
                  </motion.div>
                ))}
              </AnimatePresence>
            </button>
          )}
        </div>
        <motion.button
          type="button"
          onMouseMove={track}
          whileTap={{ scale: 0.95 }}
          transition={spring}
          onClick={add}
          className="lg-glass lg-text mb-6 mt-auto flex h-11 shrink-0 items-center gap-2 rounded-full px-5 text-[15px] font-medium"
        >
          <Bell className="relative z-[3] h-4 w-4" strokeWidth={1.75} />
          <span className="relative z-[3]">{label}</span>
        </motion.button>
      </div>
    </Stage>
  );
}
```

Give me the complete final code, ready to paste, and tell me in one line where to put it in my project.
````
