feat(cart): honey glow and drip when the drop lands in the cart

After the flying honey drop reaches the header cart icon, the basket
now lights up twice in honey colour with a warm halo, and a small
honey drop gathers at its lower edge, stretches and runs down. With
reduced motion only a single calm glow remains, without the drip.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-30 23:45:54 +02:00
1 parent 3107a02588
commit e19827d6af
2 files changed
+123 -1

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
"use client";
import { useEffect } from "react";
import { motion, useAnimationControls, useReducedMotion } from "motion/react";
import { onCartLanded } from "@/components/cart-flight";
// Der Korb im Kopfzeilen-Symbol (20 px in einem 40-px-Knopf, viewBox 24):
// seine Unterkante liegt bei y=15, die Mitte bei x=13.5. Dort bildet sich
// der Tropfen — umgerechnet in Pixel des Knopfes.
const DRIP_LEFT_PX = 10 + (13.5 / 24) * 20;
const DRIP_TOP_PX = 10 + (15 / 24) * 20;
const DRIP_WIDTH_PX = 6;
const DRIP_DELAY_S = 0.3;
/**
* Nachspiel der Landung eines Honigtropfens (components/cart-flight.tsx):
* Der Korb leuchtet zweimal warm in Honigfarbe auf, dann sammelt sich an
* seiner Unterkante ein kleiner Tropfen, zieht sich lang und perlt nach
* unten ab — als waere der Honig durch den Korb gesickert.
*
* Liegt als Overlay im Warenkorb-Knopf (relative) und faengt keine Klicks.
* Das Leuchten ist eine eigene, honigfarbene Kopie des Korbs, deren Deckkraft
* blinkt — so stimmt die Farbe im hellen wie im dunklen Farbschema, ohne die
* Strichfarbe des eigentlichen Symbols anzufassen. Bei reduzierter Bewegung
* bleibt nur ein einzelnes, ruhiges Aufleuchten ohne Tropfen.
*/
export function CartLandingEffect() {
const glowControls = useAnimationControls();
const dripControls = useAnimationControls();
const reduceMotion = useReducedMotion();
useEffect(
() =>
onCartLanded(() => {
if (reduceMotion) {
void glowControls.start({
opacity: [0, 0.9, 0],
transition: { duration: 0.8, ease: "easeInOut" },
});
return;
}
void glowControls.start({
opacity: [0, 1, 0.35, 1, 0],
scale: [0.9, 1.08, 1, 1.05, 1],
transition: { duration: 0.75, ease: "easeInOut", times: [0, 0.2, 0.45, 0.65, 1] },
});
void dripControls.start({
// An der Kante sammeln, schwer werden und lang ziehen,
// abloesen und nach unten perlen, verblassen.
opacity: [0, 1, 1, 1, 0],
scaleX: [0.3, 1, 0.85, 0.9, 0.7],
scaleY: [0.2, 1, 1.35, 1, 0.8],
y: [0, 0, 3, 13, 17],
transition: {
duration: 1.1,
delay: DRIP_DELAY_S,
ease: "easeIn",
times: [0, 0.3, 0.55, 0.85, 1],
},
});
}),
[glowControls, dripControls, reduceMotion]
);
return (
<span aria-hidden="true" className="pointer-events-none absolute inset-0">
<motion.span
initial={{ opacity: 0 }}
animate={glowControls}
className="absolute inset-0 flex items-center justify-center"
>
{/* Warmer Schein hinter dem Korb. */}
<span className="absolute inset-1 rounded-full bg-honey/30 blur-md" />
<svg
className="relative text-honey drop-shadow-[0_0_4px_var(--color-honey)]"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
>
<path d="M6 6h15l-1.5 9h-12z" fill="var(--color-honey-pale)" fillOpacity="0.55" />
<path d="M6 6 5 3H2" />
<circle cx="9" cy="20" r="1" />
<circle cx="18" cy="20" r="1" />
</svg>
</motion.span>
{reduceMotion ? null : (
<motion.span
initial={{ opacity: 0 }}
animate={dripControls}
style={{
left: DRIP_LEFT_PX - DRIP_WIDTH_PX / 2,
top: DRIP_TOP_PX - 1,
width: DRIP_WIDTH_PX,
transformOrigin: "50% 0%",
}}
className="absolute"
>
{/* Gleiche Honigtoene wie der fliegende Tropfen in cart-flight.tsx. */}
<svg viewBox="0 0 6 9" width={DRIP_WIDTH_PX} height="9">
<defs>
<radialGradient id="wh-drip" cx="40%" cy="65%" r="70%">
<stop offset="0" stopColor="#e4cfa4" />
<stop offset="0.5" stopColor="#b88a44" />
<stop offset="1" stopColor="#8a6531" />
</radialGradient>
</defs>
<path d="M3 0C3 2.5 6 4.2 6 6.1A3 3 0 0 1 0 6.1C0 4.2 3 2.5 3 0Z" fill="url(#wh-drip)" />
<ellipse cx="2" cy="5.8" rx="0.7" ry="1" fill="#fff" opacity="0.6" />
</svg>
</motion.span>
)}
</span>
);
}
+4 -1
View File
@@ -15,6 +15,7 @@ import { QuantityStepper } from "@/components/quantity-stepper";
import { LanguageSwitcher } from "@/components/language-switcher"; import { LanguageSwitcher } from "@/components/language-switcher";
import { useI18n } from "@/i18n/i18n-provider"; import { useI18n } from "@/i18n/i18n-provider";
import { CartFlight, onCartLanded } from "@/components/cart-flight"; import { CartFlight, onCartLanded } from "@/components/cart-flight";
import { CartLandingEffect } from "@/components/cart-landing-effect";
import { HexMark } from "@/components/honeycomb"; import { HexMark } from "@/components/honeycomb";
import type { Dictionary } from "@/i18n/dictionary-type"; import type { Dictionary } from "@/i18n/dictionary-type";
import { ThemeToggle } from "@/components/theme-toggle"; import { ThemeToggle } from "@/components/theme-toggle";
@@ -285,7 +286,8 @@ function CartIconButton() {
const basketControls = useAnimationControls(); const basketControls = useAnimationControls();
// Landet ein Honigtropfen (components/cart-flight.tsx), faengt der Korb // Landet ein Honigtropfen (components/cart-flight.tsx), faengt der Korb
// ihn sichtbar auf: kurzes Einsinken und Nachwippen. // ihn sichtbar auf: kurzes Einsinken und Nachwippen. Leuchten und
// abperlender Tropfen kommen aus CartLandingEffect.
useEffect( useEffect(
() => () =>
onCartLanded(() => { onCartLanded(() => {
@@ -319,6 +321,7 @@ function CartIconButton() {
<circle cx="9" cy="20" r="1" /> <circle cx="9" cy="20" r="1" />
<circle cx="18" cy="20" r="1" /> <circle cx="18" cy="20" r="1" />
</motion.svg> </motion.svg>
<CartLandingEffect />
<AnimatePresence> <AnimatePresence>
{count > 0 ? ( {count > 0 ? (
<motion.span <motion.span