"use client";
import { motion } from "framer-motion";
import { GripHorizontal } from "lucide-react";
import { useTheme } from "next-themes";
import React, { useCallback, useEffect, useState } from "react";
import { cn } from "@/lib/utils";
const Skiper26 = () => {
const [variant, setVariant] = useState<AnimationVariant>("rectangle");
const [start, setStart] = useState<AnimationStart>("bottom-up");
const [blur, setBlur] = useState<boolean>(false);
const [gifType, setGifType] = useState<"1" | "2" | "3" | "custom">("1");
const [gifUrl, setGifUrl] = useState<string>(
"https://media.giphy.com/media/KBbr4hHl9DSahKvInO/giphy.gif?cid=790b76112m5eeeydoe7et0cr3j3ekb1erunxozyshuhxx2vl&ep=v1_stickers_search&rid=giphy.gif&ct=s",
);
return (
<div className="relative flex h-full w-full flex-col items-center justify-center">
<div className="mx-auto max-w-lg space-y-5">
<h2 className="mt-36 text-4xl font-medium tracking-tight">
07.09.2025 <br />
Skiper ui is live now
</h2>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nihil, ex
eligendi veniam praesentium temporibus natus quae laborum nemo
repellendus cum!
</p>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nihil, ex
eligendi veniam praesentium temporibus natus quae laborum nemo
repellendus cum!
</p>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nihil, ex
eligendi veniam praesentium temporibus natus quae laborum nemo
repellendus cum!
</p>
</div>
<div className="text-foreground grid content-start justify-items-center gap-6 py-20 text-center">
<span className="after:from-background after:to-foreground relative max-w-[12ch] text-xs uppercase leading-tight opacity-40 after:absolute after:left-1/2 after:top-full after:h-16 after:w-px after:bg-gradient-to-b after:content-['']">
Click to toggle the theme
</span>
</div>
<ThemeToggleButton
variant={variant}
start={start}
blur={blur}
gifUrl={gifUrl}
/>
<Options
variant={variant}
start={start}
blur={blur}
gifType={gifType}
gifUrl={gifUrl}
setVariant={setVariant}
setStart={setStart}
setBlur={setBlur}
setGifType={setGifType}
setGifUrl={setGifUrl}
/>
</div>
);
};
export { Skiper26 };
const Options = ({
variant,
start,
blur,
gifType,
gifUrl,
setVariant,
setStart,
setBlur,
setGifType,
setGifUrl,
}: {
variant: AnimationVariant;
start: AnimationStart;
blur: boolean;
gifType: "1" | "2" | "3" | "custom";
gifUrl: string;
setVariant: (variant: AnimationVariant) => void;
setStart: (start: AnimationStart) => void;
setBlur: (blur: boolean) => void;
setGifType: (type: "1" | "2" | "3" | "custom") => void;
setGifUrl: (url: string) => void;
}) => {
return (
<motion.div
drag
className="top-30 border-foreground/10 bg-muted2 absolute right-1/2 flex w-[245px] translate-x-1/2 flex-col gap-3 rounded-3xl border p-3 backdrop-blur-sm lg:right-4 lg:translate-x-0"
>
<div className="flex items-center justify-between">
<span className="size-4 cursor-grab active:cursor-grabbing">
<GripHorizontal className="size-4 opacity-50" />
</span>
<p className="group flex cursor-pointer items-center justify-center gap-1 rounded-lg px-2 py-1 text-sm opacity-50">
Options
</p>
</div>
<div className="flex flex-col">
<div className="mt-1 flex justify-between py-1">
<p className="w-20 whitespace-nowrap text-sm opacity-50">variant :</p>
<div className="flex flex-wrap items-center justify-end gap-1">
<button
onClick={() => setVariant("circle")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
variant === "circle"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
circle
</button>
<button
onClick={() => setVariant("rectangle")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
variant === "rectangle"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
rectangle
</button>
<button
onClick={() => setVariant("gif")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
variant === "gif"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
gif
</button>
<button
onClick={() => setVariant("polygon")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
variant === "polygon"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
polygon
</button>
<button
onClick={() => setVariant("circle-blur")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
variant === "circle-blur"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
circle-blur
</button>
</div>
</div>
<div className="mt-1 flex justify-between py-1">
<p className="w-20 whitespace-nowrap text-sm opacity-50">blur :</p>
<div className="flex flex-wrap items-center justify-end gap-1">
<button
onClick={() => setBlur(false)}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
!blur
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
off
</button>
<button
onClick={() => setBlur(true)}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
blur
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
on
</button>
</div>
</div>
{/* Show start options for circle, rectangle, polygon, and circle-blur */}
{(variant === "circle" ||
variant === "rectangle" ||
variant === "polygon" ||
variant === "circle-blur") && (
<div className="mt-1 flex justify-between py-1">
<p className="w-20 whitespace-nowrap text-sm opacity-50">start :</p>
<div className="flex flex-wrap items-center justify-end gap-1">
{/* Show center option only for circle and circle-blur */}
{(variant === "circle" || variant === "circle-blur") && (
<button
onClick={() => setStart("center")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "center"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
center
</button>
)}
{/* Show directional options for rectangle */}
{variant === "rectangle" && (
<>
<button
onClick={() => setStart("bottom-up")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "bottom-up"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
bottom-up
</button>
<button
onClick={() => setStart("top-down")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "top-down"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
top-down
</button>
<button
onClick={() => setStart("left-right")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "left-right"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
left-right
</button>
<button
onClick={() => setStart("right-left")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "right-left"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
right-left
</button>
</>
)}
{/* Show corner options for circle, polygon, and circle-blur variants */}
{(variant === "circle" ||
variant === "polygon" ||
variant === "circle-blur") && (
<>
<button
onClick={() => setStart("top-left")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "top-left"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
top-left
</button>
<button
onClick={() => setStart("top-right")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "top-right"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
top-right
</button>
{/* Only show bottom corners for circle, not polygon */}
{variant !== "polygon" && (
<>
<button
onClick={() => setStart("bottom-left")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "bottom-left"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
bottom-left
</button>
<button
onClick={() => setStart("bottom-right")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "bottom-right"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
bottom-right
</button>
</>
)}
</>
)}
{/* Show center options for circle and circle-blur */}
{(variant === "circle" || variant === "circle-blur") && (
<>
<button
onClick={() => setStart("top-center")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "top-center"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
top-center
</button>
<button
onClick={() => setStart("bottom-center")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
start === "bottom-center"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
bottom-center
</button>
</>
)}
</div>
</div>
)}
{/* Show gif type options only for gif variant */}
{variant === "gif" && (
<div className="mt-1 flex justify-between py-1">
<p className="w-20 text-sm opacity-50">gif type :</p>
<div className="flex flex-wrap items-center justify-end gap-1">
<button
onClick={() => {
setGifType("1");
setGifUrl(
"https://media.giphy.com/media/KBbr4hHl9DSahKvInO/giphy.gif?cid=790b76112m5eeeydoe7et0cr3j3ekb1erunxozyshuhxx2vl&ep=v1_stickers_search&rid=giphy.gif&ct=s",
);
}}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
gifType === "1"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
1
</button>
<button
onClick={() => {
setGifType("2");
setGifUrl(
"https://media.giphy.com/media/5PncuvcXbBuIZcSiQo/giphy.gif?cid=ecf05e47j7vdjtytp3fu84rslaivdun4zvfhej6wlvl6qqsz&ep=v1_stickers_search&rid=giphy.gif&ct=s",
);
}}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
gifType === "2"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
2
</button>
<button
onClick={() => {
setGifType("3");
setGifUrl(
"https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3JwcXdzcHd5MW92NWprZXVpcTBtNXM5cG9obWh0N3I4NzFpaDE3byZlcD12MV9zdGlja2Vyc19zZWFyY2gmY3Q9cw/WgsVx6C4N8tjy/giphy.gif",
);
}}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
gifType === "3"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
3
</button>
<button
onClick={() => setGifType("custom")}
className={cn(
"cursor-pointer px-1 text-sm transition-opacity",
gifType === "custom"
? "opacity-100"
: "hover:bg-foreground/10 opacity-50 hover:opacity-100",
)}
>
custom
</button>
</div>
</div>
)}
{/* Show input only when gif variant and custom type are selected */}
{variant === "gif" && gifType === "custom" && (
<div className="mt-1 flex flex-col gap-1 py-1">
<p className="text-sm opacity-50">gif url :</p>
<input
type="text"
value={gifUrl}
onChange={(e) => setGifUrl(e.target.value)}
placeholder="Enter GIF URL"
className="text-foreground placeholder:text-foreground/50 w-full rounded-lg bg-transparent px-2 py-1 text-xs focus:outline-none"
/>
</div>
)}
</div>
</motion.div>
);
};
// ///////////////////////////////////////////////////////////////////////////
// Custom hook for theme toggle functionality
export const useThemeToggle = ({
variant = "circle",
start = "center",
blur = false,
gifUrl = "",
}: {
variant?: AnimationVariant;
start?: AnimationStart;
blur?: boolean;
gifUrl?: string;
} = {}) => {
const { theme, setTheme, resolvedTheme } = useTheme();
const [isDark, setIsDark] = useState(false);
// Sync isDark state with resolved theme after hydration
useEffect(() => {
setIsDark(resolvedTheme === "dark");
}, [resolvedTheme]);
const styleId = "theme-transition-styles";
const updateStyles = useCallback((css: string, name: string) => {
if (typeof window === "undefined") return;
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
styleElement.textContent = css;
}, []);
const toggleTheme = useCallback(() => {
setIsDark(!isDark);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css, animation.name);
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [
theme,
setTheme,
variant,
start,
blur,
gifUrl,
updateStyles,
isDark,
setIsDark,
]);
const setCrazyLightTheme = useCallback(() => {
setIsDark(false);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css, animation.name);
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme("light");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles, setIsDark]);
const setCrazyDarkTheme = useCallback(() => {
setIsDark(true);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css, animation.name);
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme("dark");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles, setIsDark]);
const setCrazySystemTheme = useCallback(() => {
if (typeof window === "undefined") return;
// Check system preference for dark mode
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
setIsDark(prefersDark);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css, animation.name);
const switchTheme = () => {
setTheme("system");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles, setIsDark]);
return {
isDark,
setIsDark,
toggleTheme,
setCrazyLightTheme,
setCrazyDarkTheme,
setCrazySystemTheme,
};
};
// ///////////////////////////////////////////////////////////////////////////
export const ThemeToggleButton = ({
className = "",
variant = "circle",
start = "center",
blur = false,
gifUrl = "",
}: {
className?: string;
variant?: AnimationVariant;
start?: AnimationStart;
blur?: boolean;
gifUrl?: string;
}) => {
const { isDark, toggleTheme } = useThemeToggle({
variant,
start,
blur,
gifUrl,
});
return (
<button
type="button"
className={cn(
"size-10 cursor-pointer rounded-full bg-black p-0 transition-all duration-300 active:scale-95",
className,
)}
onClick={toggleTheme}
aria-label="Toggle theme"
>
<span className="sr-only">Toggle theme</span>
<svg viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<motion.g
animate={{ rotate: isDark ? -180 : 0 }}
transition={{ ease: "easeInOut", duration: 0.5 }}
>
<path
d="M120 67.5C149.25 67.5 172.5 90.75 172.5 120C172.5 149.25 149.25 172.5 120 172.5"
fill="white"
/>
<path
d="M120 67.5C90.75 67.5 67.5 90.75 67.5 120C67.5 149.25 90.75 172.5 120 172.5"
fill="black"
/>
</motion.g>
<motion.path
animate={{ rotate: isDark ? 180 : 0 }}
transition={{ ease: "easeInOut", duration: 0.5 }}
d="M120 3.75C55.5 3.75 3.75 55.5 3.75 120C3.75 184.5 55.5 236.25 120 236.25C184.5 236.25 236.25 184.5 236.25 120C236.25 55.5 184.5 3.75 120 3.75ZM120 214.5V172.5C90.75 172.5 67.5 149.25 67.5 120C67.5 90.75 90.75 67.5 120 67.5V25.5C172.5 25.5 214.5 67.5 214.5 120C214.5 172.5 172.5 214.5 120 214.5Z"
fill="white"
/>
</svg>
</button>
);
};
// ///////////////////////////////////////////////////////////////////////////
export type AnimationVariant =
| "circle"
| "rectangle"
| "gif"
| "polygon"
| "circle-blur";
export type AnimationStart =
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right"
| "center"
| "top-center"
| "bottom-center"
| "bottom-up"
| "top-down"
| "left-right"
| "right-left";
interface Animation {
name: string;
css: string;
}
const getPositionCoords = (position: AnimationStart) => {
switch (position) {
case "top-left":
return { cx: "0", cy: "0" };
case "top-right":
return { cx: "40", cy: "0" };
case "bottom-left":
return { cx: "0", cy: "40" };
case "bottom-right":
return { cx: "40", cy: "40" };
case "top-center":
return { cx: "20", cy: "0" };
case "bottom-center":
return { cx: "20", cy: "40" };
// For directional positions, default to center (these are used for rectangle variant)
case "bottom-up":
case "top-down":
case "left-right":
case "right-left":
return { cx: "20", cy: "20" };
}
};
const generateSVG = (variant: AnimationVariant, start: AnimationStart) => {
// circle-blur variant handles center case differently, so check it first
if (variant === "circle-blur") {
if (start === "center") {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="20" cy="20" r="18" fill="white" filter="url(%23blur)"/></svg>`;
}
const positionCoords = getPositionCoords(start);
if (!positionCoords) {
throw new Error(`Invalid start position: ${start}`);
}
const { cx, cy } = positionCoords;
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="${cx}" cy="${cy}" r="18" fill="white" filter="url(%23blur)"/></svg>`;
}
if (start === "center") return;
// Rectangle variant doesn't use SVG masks, so return early
if (variant === "rectangle") return "";
const positionCoords = getPositionCoords(start);
if (!positionCoords) {
throw new Error(`Invalid start position: ${start}`);
}
const { cx, cy } = positionCoords;
if (variant === "circle") {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="${cx}" cy="${cy}" r="20" fill="white"/></svg>`;
}
return "";
};
const getTransformOrigin = (start: AnimationStart) => {
switch (start) {
case "top-left":
return "top left";
case "top-right":
return "top right";
case "bottom-left":
return "bottom left";
case "bottom-right":
return "bottom right";
case "top-center":
return "top center";
case "bottom-center":
return "bottom center";
// For directional positions, default to center
case "bottom-up":
case "top-down":
case "left-right":
case "right-left":
return "center";
}
};
export const createAnimation = (
variant: AnimationVariant,
start: AnimationStart = "center",
blur = false,
url?: string,
): Animation => {
const svg = generateSVG(variant, start);
const transformOrigin = getTransformOrigin(start);
if (variant === "rectangle") {
const getClipPath = (direction: AnimationStart) => {
switch (direction) {
case "bottom-up":
return {
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-down":
return {
from: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "left-right":
return {
from: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "right-left":
return {
from: "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-left":
return {
from: "polygon(0% 0%, 0% 0%, 0% 0%, 0% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-right":
return {
from: "polygon(100% 0%, 100% 0%, 100% 0%, 100% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "bottom-left":
return {
from: "polygon(0% 100%, 0% 100%, 0% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "bottom-right":
return {
from: "polygon(100% 100%, 100% 100%, 100% 100%, 100% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
default:
return {
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
}
};
const clipPath = getClipPath(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
animation-name: reveal-light-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
@keyframes reveal-dark-${start}${blur ? "-blur" : ""} {
from {
clip-path: ${clipPath.from};
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: ${clipPath.to};
${blur ? "filter: blur(0px);" : ""}
}
}
@keyframes reveal-light-${start}${blur ? "-blur" : ""} {
from {
clip-path: ${clipPath.from};
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: ${clipPath.to};
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
}
if (variant === "circle" && start == "center") {
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
animation-name: reveal-light${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
@keyframes reveal-dark${blur ? "-blur" : ""} {
from {
clip-path: circle(0% at 50% 50%);
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: circle(100.0% at 50% 50%);
${blur ? "filter: blur(0px);" : ""}
}
}
@keyframes reveal-light${blur ? "-blur" : ""} {
from {
clip-path: circle(0% at 50% 50%);
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: circle(100.0% at 50% 50%);
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
}
if (variant === "gif") {
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-in);
}
::view-transition-new(root) {
mask: url('${url}') center / 0 no-repeat;
animation: scale 3s;
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: scale 3s;
}
@keyframes scale {
0% {
mask-size: 0;
}
10% {
mask-size: 50vmax;
}
90% {
mask-size: 50vmax;
}
100% {
mask-size: 2000vmax;
}
}`,
};
}
if (variant === "circle-blur") {
if (start === "center") {
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
mask: url('${svg}') center / 0 no-repeat;
mask-origin: content-box;
animation: scale 1s;
transform-origin: center;
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: scale 1s;
transform-origin: center;
z-index: -1;
}
@keyframes scale {
to {
mask-size: 350vmax;
}
}
`,
};
}
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
mask: url('${svg}') ${start.replace("-", " ")} / 0 no-repeat;
mask-origin: content-box;
animation: scale 1s;
transform-origin: ${transformOrigin};
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: scale 1s;
transform-origin: ${transformOrigin};
z-index: -1;
}
@keyframes scale {
to {
mask-size: 350vmax;
}
}
`,
};
}
if (variant === "polygon") {
const getPolygonClipPaths = (position: AnimationStart) => {
switch (position) {
case "top-left":
return {
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
};
case "top-right":
return {
darkFrom: "polygon(150% -71%, 250% 71%, 250% 71%, 150% -71%)",
darkTo: "polygon(150% -71%, 250% 71%, 50% 171%, -71% 50%)",
lightFrom: "polygon(-71% 50%, 50% 171%, 50% 171%, -71% 50%)",
lightTo: "polygon(-71% 50%, 50% 171%, 250% 71%, 150% -71%)",
};
default:
// Default to top-left behavior
return {
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
};
}
};
const clipPaths = getPolygonClipPaths(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
animation-name: reveal-light-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
@keyframes reveal-dark-${start}${blur ? "-blur" : ""} {
from {
clip-path: ${clipPaths.darkFrom};
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: ${clipPaths.darkTo};
${blur ? "filter: blur(0px);" : ""}
}
}
@keyframes reveal-light-${start}${blur ? "-blur" : ""} {
from {
clip-path: ${clipPaths.lightFrom};
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: ${clipPaths.lightTo};
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
}
// Handle circle variants with start positions using clip-path
if (variant === "circle" && start !== "center") {
const getClipPathPosition = (position: AnimationStart) => {
switch (position) {
case "top-left":
return "0% 0%";
case "top-right":
return "100% 0%";
case "bottom-left":
return "0% 100%";
case "bottom-right":
return "100% 100%";
case "top-center":
return "50% 0%";
case "bottom-center":
return "50% 100%";
default:
return "50% 50%";
}
};
const clipPosition = getClipPathPosition(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 1s;
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
animation-name: reveal-light-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
@keyframes reveal-dark-${start}${blur ? "-blur" : ""} {
from {
clip-path: circle(0% at ${clipPosition});
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: circle(150.0% at ${clipPosition});
${blur ? "filter: blur(0px);" : ""}
}
}
@keyframes reveal-light-${start}${blur ? "-blur" : ""} {
from {
clip-path: circle(0% at ${clipPosition});
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: circle(150.0% at ${clipPosition});
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
}
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-in);
}
::view-transition-new(root) {
mask: url('${svg}') ${start.replace("-", " ")} / 0 no-repeat;
mask-origin: content-box;
animation: scale-${start}${blur ? "-blur" : ""} 1s;
transform-origin: ${transformOrigin};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: scale-${start}${blur ? "-blur" : ""} 1s;
transform-origin: ${transformOrigin};
z-index: -1;
}
@keyframes scale-${start}${blur ? "-blur" : ""} {
from {
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
mask-size: 2000vmax;
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
};
/**
* Skiper 26 Theme_buttons_002 — React + CSS + transition view api https://developer.chrome.com/docs/web-platform/view-transitions/
* Orignal concept from rudrodip
* Inspired by from https://github.com/rudrodip/theme-toggle-effect
* We respect the original creators. This is an inspired rebuild with our own taste and does not claim any ownership.
* These animations aren’t associated with the rudrodip . They’re independent recreations meant to study interaction design
*
* License & Usage:
* - Free to use and modify in both personal and commercial projects.
* - Attribution to Skiper UI is required when using the free version.
* - No attribution required with Skiper UI Pro.
*
* Feedback and contributions are welcome.
*
* Author: @gurvinder-singh02
* Website: https://gxuri.in
* Twitter: https://x.com/Gur__vi
*/