Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/www/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,12 @@ export const docsConfig: DocsConfig = {
items: [],
label: "",
},
{
title: "Curtain Loader",
href: `/docs/components/curtain-loader`,
items: [],
label: "New",
},
],
},
{
Expand Down
84 changes: 84 additions & 0 deletions apps/www/content/docs/components/curtain-loader.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Curtain Loader
date: 2026-05-02
description: A curtain-style loader animation.
author: Aniket
published: true
---

<ComponentPreview name="curtain-loader-demo" />

## Installation

<Tabs defaultValue="cli">

<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add "https://magicui.design/r/curtain-loader"
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="curtain-loader" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

```tsx showLineNumbers
import { useState } from "react"
import { AnimatePresence } from "framer-motion"

import CurtainLoader from "@/components/magicui/curtain-loader"
```

```tsx showLineNumbers
export default function MyComponent() {
const [isLoading, setIsLoading] = useState(true)

return (
<>
<AnimatePresence>
{isLoading && (
<CurtainLoader
greetings={["Hello", "Welcome", "Bonjour"]}
speed={200}
onComplete={() => setIsLoading(false)}
/>
)}
</AnimatePresence>

{!isLoading && <div>Your main app content goes here!</div>}
</>
)
}
```

## Props

| Prop | Type | Default | Description |
| ------------ | ------------ | ------- | ----------------------------------------------------------------------------------- |
| `onComplete` | `() => void` | | Callback triggered when the animation finishes. **Required** to unmount the loader. |
| `greetings` | `string[]` | | Array of greetings to display in sequence. |
| `speed` | `number` | `150` | The speed (in ms) at which greetings cycle. |

## Credits

Built by [@Aniket-a14](https://github.com/Aniket-a14).
7 changes: 7 additions & 0 deletions apps/www/content/showcase/aniketdesign.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Aniket Design
description: Personal portfolio and design showcase.
image: /showcase/aniketdesign.png
href: https://www.aniketdesign.in/
featured: true
---
252 changes: 252 additions & 0 deletions apps/www/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5897,6 +5897,184 @@ export default function CoolModeCustom() {



===== COMPONENT: curtain-loader =====
Title: Curtain Loader
Description: A full-screen curtain-style loader animation.

--- file: magicui/curtain-loader.tsx ---
"use client"

import React, { useEffect, useState } from "react"
import { motion } from "framer-motion"

export interface LoaderProps {
onComplete?: () => void
greetings?: string[]
speed?: number
}

export default function CurtainLoader({
onComplete,
greetings = [
"• Hi",
"• Hola",
"• Bonjour",
"• Hallo",
"• Ciao",
"• Sawasdee",
"• Aloha",
"• Salaam",
"• হ্যালো",
"• Namaste",
],
speed = 150,
}: LoaderProps) {
const [moveUp, setMoveUp] = useState(false)
const [greetingIndex, setGreetingIndex] = useState(0)

useEffect(() => {
const hasVisited = sessionStorage.getItem("hasVisited")
const delayBeforeGreetings = 500
let greetingInterval: NodeJS.Timeout

const startGreetings = setTimeout(() => {
let index = 0
const maxGreetings = hasVisited ? 1 : greetings.length

greetingInterval = setInterval(() => {
index++
if (index < maxGreetings) {
setGreetingIndex(index)
} else {
clearInterval(greetingInterval)
if (!hasVisited) sessionStorage.setItem("hasVisited", "true")
setTimeout(() => setMoveUp(true), 100)
setTimeout(() => {
onComplete?.()
}, 200)
}
}, speed)
}, delayBeforeGreetings)

return () => {
clearTimeout(startGreetings)
if (greetingInterval) clearInterval(greetingInterval)
}
}, [onComplete, greetings.length, speed])

return (
<>
<motion.div
exit={{ opacity: 0, transition: { duration: 0.8 } }}
className="absolute inset-0 z-40 bg-black"
/>
<motion.div
initial={{ y: 0, scale: 1 }}
exit={{
y: "-100%",
scale: 1.05,
transition: { duration: 0.8, ease: [0.76, 0, 0.24, 1] },
}}
className="absolute top-0 right-0 bottom-0 left-0 z-50 flex flex-col items-center justify-center bg-black shadow-[0_4px_30px_rgba(0,0,0,0.5)]"
role="status"
aria-live="polite"
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
willChange: "transform, opacity",
}}
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="mb-4 text-5xl font-medium text-white"
>
{greetings[greetingIndex]}
</motion.div>

<div className="absolute bottom-0 h-6 w-full shadow-lg" />
</motion.div>
</>
)
}


===== EXAMPLE: curtain-loader-demo =====
Title: Curtain Loader Demo

--- file: example/curtain-loader-demo.tsx ---
"use client"

import React, { useEffect, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"

import CurtainLoader from "@/registry/magicui/curtain-loader"
import { RainbowButton } from "@/registry/magicui/rainbow-button"

export default function CurtainLoaderDemo() {
const [isLoading, setIsLoading] = useState(true)

useEffect(() => {
sessionStorage.removeItem("hasVisited")
}, [])

const handleReplay = () => {
sessionStorage.removeItem("hasVisited")
setIsLoading(true)
}

return (
<div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border md:shadow-xl">
<AnimatePresence>
{isLoading && <CurtainLoader onComplete={() => setIsLoading(false)} />}
</AnimatePresence>

{!isLoading && (
<motion.div
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
transition={{ duration: 1, delay: 0.2, ease: "easeOut" }}
className="z-10 flex flex-col items-center gap-8 text-center"
>
<div className="flex flex-col items-center gap-3">
<h2 className="text-3xl font-medium tracking-tight text-zinc-900 sm:text-4xl dark:text-zinc-50">
You are Welcome!
</h2>
<p className="text-zinc-500 dark:text-zinc-400">
Click the button below to replay the animation.
</p>
</div>

<div className="mt-4">
<RainbowButton onClick={handleReplay} className="gap-2">
<svg
className="h-4 w-4 transition-transform group-hover:-rotate-180"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
</svg>
Replay
</RainbowButton>
</div>
</motion.div>
)}

<div className="pointer-events-none absolute inset-0 -z-10 h-full w-full bg-white dark:bg-zinc-950">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(0,0,0,0.05)_0%,transparent_100%)] dark:bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.05)_0%,transparent_100%)]" />
</div>
</div>
)
}



===== COMPONENT: dia-text-reveal =====
Title: Dia Text Reveal
Description: A horizontal color band sweeps across text, revealing a gradient shine before settling on the base color.
Expand Down Expand Up @@ -14669,6 +14847,80 @@ export default function RainbowButtonDemo() {
}


===== EXAMPLE: curtain-loader-demo =====
Title: Curtain Loader Demo

--- file: example/curtain-loader-demo.tsx ---
"use client"

import React, { useEffect, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"

import CurtainLoader from "@/registry/magicui/curtain-loader"
import { RainbowButton } from "@/registry/magicui/rainbow-button"

export default function CurtainLoaderDemo() {
const [isLoading, setIsLoading] = useState(true)

useEffect(() => {
sessionStorage.removeItem("hasVisited")
}, [])

const handleReplay = () => {
sessionStorage.removeItem("hasVisited")
setIsLoading(true)
}

return (
<div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border md:shadow-xl">
<AnimatePresence>
{isLoading && <CurtainLoader onComplete={() => setIsLoading(false)} />}
</AnimatePresence>

{!isLoading && (
<motion.div
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
transition={{ duration: 1, delay: 0.2, ease: "easeOut" }}
className="z-10 flex flex-col items-center gap-8 text-center"
>
<div className="flex flex-col items-center gap-3">
<h2 className="text-3xl font-medium tracking-tight text-zinc-900 sm:text-4xl dark:text-zinc-50">
You are Welcome!
</h2>
<p className="text-zinc-500 dark:text-zinc-400">
Click the button below to replay the animation.
</p>
</div>

<div className="mt-4">
<RainbowButton onClick={handleReplay} className="gap-2">
<svg
className="h-4 w-4 transition-transform group-hover:-rotate-180"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
</svg>
Replay
</RainbowButton>
</div>
</motion.div>
)}

<div className="pointer-events-none absolute inset-0 -z-10 h-full w-full bg-white dark:bg-zinc-950">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(0,0,0,0.05)_0%,transparent_100%)] dark:bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.05)_0%,transparent_100%)]" />
</div>
</div>
)
}



===== COMPONENT: retro-grid =====
Title: Retro Grid
Expand Down
2 changes: 2 additions & 0 deletions apps/www/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This file provides LLM-friendly entry points to documentation and examples.
- [Comic Text](https://magicui.design/docs/components/comic-text): Comic text animation
- [Confetti](https://magicui.design/docs/components/confetti): Confetti animations are best used to delight your users when something special happens
- [Cool Mode](https://magicui.design/docs/components/cool-mode): Cool mode effect for buttons, links, and other DOMs
- [Curtain Loader](https://magicui.design/docs/components/curtain-loader): A full-screen curtain-style loader animation.
- [Dia Text Reveal](https://magicui.design/docs/components/dia-text-reveal): A horizontal color band sweeps across text, revealing a gradient shine before settling on the base color.
- [Dock](https://magicui.design/docs/components/dock): An implementation of the MacOS dock using react + tailwindcss + motion
- [Dot Pattern](https://magicui.design/docs/components/dot-pattern): A background dot pattern made with SVGs, fully customizable using Tailwind CSS.
Expand Down Expand Up @@ -249,6 +250,7 @@ This file provides LLM-friendly entry points to documentation and examples.
- [backlight-svg-demo](https://github.com/magicuidesign/magicui/blob/main/example/backlight-svg-demo.tsx): Example usage
- [Text 3D Flip Demo](https://github.com/magicuidesign/magicui/blob/main/example/text-3d-flip-demo.tsx): Example usage
- [Text 3D Flip Demo 2](https://github.com/magicuidesign/magicui/blob/main/example/text-3d-flip-demo-2.tsx): Example usage
- [Curtain Loader Demo](https://github.com/magicuidesign/magicui/blob/main/example/curtain-loader-demo.tsx): Example usage

## Optional

Expand Down
Loading
Loading